Files
aimaren/vendor/github.com/nlnwa/whatwg-url/url/parser.go
T
2025-07-26 05:58:59 +00:00

933 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2020 National Library of Norway.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package url
import (
goerrors "errors"
u2 "net/url"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/bits-and-blooms/bitset"
"github.com/nlnwa/whatwg-url/errors"
)
func NewParser(opts ...ParserOption) Parser {
p := &parser{opts: defaultParserOptions()}
for _, opt := range opts {
opt.apply(&p.opts)
}
return p
}
type Parser interface {
Parse(rawUrl string) (*Url, error)
ParseRef(rawUrl, ref string) (*Url, error)
BasicParser(urlOrRef string, base *Url, url *Url, stateOverride State) (*Url, error)
PercentEncodeString(s string, tr *PercentEncodeSet) string
NewUrl() *Url
}
type parser struct {
opts parserOptions
}
func (p *parser) Parse(rawUrl string) (*Url, error) {
return p.BasicParser(rawUrl, nil, nil, NoState)
}
func (p *parser) ParseRef(rawUrl, ref string) (*Url, error) {
if rawUrl == "" {
return p.Parse(ref)
}
b, err := p.Parse(rawUrl)
if err != nil {
return nil, err
}
return p.BasicParser(ref, b, nil, NoState)
}
func (u *Url) Parse(ref string) (*Url, error) {
return u.parser.BasicParser(ref, u, nil, NoState)
}
var defaultParser = NewParser()
func Parse(rawUrl string) (*Url, error) {
return defaultParser.Parse(rawUrl)
}
func ParseRef(rawUrl, ref string) (*Url, error) {
return defaultParser.ParseRef(rawUrl, ref)
}
type State int
const (
NoState State = iota
StateSchemeStart
StateScheme
StateNoScheme
StateOpaquePath
StateSpecialRelativeOrAuthority
StateSpecialAuthoritySlashes
StateSpecialAuthorityIgnoreSlashes
StatePathOrAuthority
StateAuthority
StateHost
StateHostname
StateFile
StateFileHost
StateFileSlash
StatePort
StatePath
StatePathStart
StateQuery
StateFragment
StateRelative
StateRelativeSlash
)
// BasicParser implements WHATWG basic URL parser (https://url.spec.whatwg.org/#concept-basic-url-parser)
// In most cases, when possible, prefer using the higher level Parse method.
func (p *parser) BasicParser(urlOrRef string, baseUrl *Url, url *Url, stateOverride State) (*Url, error) {
stateOverridden := stateOverride > NoState
if url == nil {
url = &Url{inputUrl: urlOrRef, path: &path{}}
if i, changed := trim(url.inputUrl, C0OrSpacePercentEncodeSet); changed {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
url.inputUrl = i
}
} else {
url.inputUrl = urlOrRef
}
url.parser = p
if i, changed := remove(url.inputUrl, ASCIITabOrNewline); changed {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
url.inputUrl = i
}
input := newInputString(url.inputUrl)
var state State
if stateOverridden {
state = stateOverride
} else {
state = StateSchemeStart
}
var buffer strings.Builder
atFlag := false
bracketFlag := false
passwordTokenSeenFlag := false
var base *Url
if baseUrl != nil {
base = baseUrl.Clone()
}
for {
r := input.nextCodePoint()
switch state {
case StateSchemeStart:
if ASCIIAlpha.Test(uint(r)) {
buffer.WriteRune(unicode.ToLower(r))
state = StateScheme
} else if !stateOverridden {
state = StateNoScheme
input.rewindLast()
} else {
if err := p.handleError(url, errors.InvalidURLUnit, true); err != nil {
return nil, err
}
}
case StateScheme:
tr := ASCIIAlphanumeric.Clone().Set(0x2b).Set(0x2d).Set(0x2e)
if tr.Test(uint(r)) {
buffer.WriteRune(unicode.ToLower(r))
} else if r == ':' {
if stateOverridden {
// If urls scheme is a special scheme and buffer is not a special scheme, then return.
if url.isSpecialScheme(url.scheme) && !url.isSpecialScheme(buffer.String()) {
return url, nil
}
// If urls scheme is not a special scheme and buffer is a special scheme, then return.
if !url.isSpecialScheme(url.scheme) && url.isSpecialScheme(buffer.String()) {
return url, nil
}
// If url includes credentials or has a non-null port, and buffer is "file", then return.
if (url.username != "" || url.password != "" || url.port != nil) && buffer.String() == "file" {
return url, nil
}
// If urls scheme is "file" and its host is an empty host or null, then return.
if url.scheme == "file" && *url.host == "" {
return url, nil
}
}
url.scheme = buffer.String()
if stateOverridden {
url.cleanDefaultPort()
return url, nil
}
buffer.Reset()
if url.scheme == "file" {
if !input.remainingStartsWith("//") {
if err := p.handleError(url, errors.SpecialSchemeMissingFollowingSolidus, false); err != nil {
return nil, err
}
}
state = StateFile
} else if url.IsSpecialScheme() && base != nil && base.scheme == url.scheme {
state = StateSpecialRelativeOrAuthority
} else if url.IsSpecialScheme() {
state = StateSpecialAuthoritySlashes
} else if input.remainingStartsWith("/") {
state = StatePathOrAuthority
input.nextCodePoint()
} else {
url.path.setOpaque("")
state = StateOpaquePath
}
} else if !stateOverridden {
buffer.Reset()
state = StateNoScheme
input.reset()
} else {
if err := p.handleError(url, errors.InvalidURLUnit, true); err != nil {
return nil, err
}
}
case StateNoScheme:
if base == nil || (base.path.isOpaque() && r != '#') {
if err := p.handleError(url, errors.MissingSchemeNonRelativeURL, true); err != nil {
return nil, err
}
} else if base.path.isOpaque() && r == '#' {
url.scheme = base.scheme
url.path = base.path
url.query = base.query
url.fragment = new(string)
state = StateFragment
} else if base.scheme != "file" {
state = StateRelative
input.rewindLast()
} else {
state = StateFile
input.rewindLast()
}
case StateSpecialRelativeOrAuthority:
if r == '/' && input.remainingStartsWith("/") {
state = StateSpecialAuthorityIgnoreSlashes
input.nextCodePoint()
} else {
if err := p.handleError(url, errors.SpecialSchemeMissingFollowingSolidus, false); err != nil {
return nil, err
}
state = StateRelative
input.rewindLast()
}
case StatePathOrAuthority:
if r == '/' {
state = StateAuthority
} else {
state = StatePath
input.rewindLast()
}
case StateRelative:
url.scheme = base.scheme
if r == '/' {
state = StateRelativeSlash
} else if url.isSpecialSchemeAndBackslash(r) {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
state = StateRelativeSlash
} else {
url.username = base.username
url.password = base.password
url.host = base.host
url.port = base.port
url.decodedPort = base.decodedPort
url.path = base.path
url.query = base.query
if r == '?' {
url.query = new(string)
state = StateQuery
} else if r == '#' {
url.fragment = new(string)
state = StateFragment
} else if !input.eof {
url.query = nil
url.path.shortenPath(url.scheme)
state = StatePath
input.rewindLast()
}
}
case StateRelativeSlash:
if url.IsSpecialScheme() && (r == '/' || r == '\\') {
if r == '\\' {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
}
state = StateSpecialAuthorityIgnoreSlashes
} else if r == '/' {
state = StateAuthority
} else {
url.username = base.username
url.password = base.password
url.host = base.host
url.port = base.port
url.decodedPort = base.decodedPort
state = StatePath
input.rewindLast()
}
case StateSpecialAuthoritySlashes:
if r == '/' && input.remainingStartsWith("/") {
state = StateSpecialAuthorityIgnoreSlashes
input.nextCodePoint()
} else {
if err := p.handleError(url, errors.SpecialSchemeMissingFollowingSolidus, false); err != nil {
return nil, err
}
state = StateSpecialAuthorityIgnoreSlashes
input.rewindLast()
}
case StateSpecialAuthorityIgnoreSlashes:
if r != '/' && r != '\\' {
state = StateAuthority
input.rewindLast()
} else {
if err := p.handleError(url, errors.SpecialSchemeMissingFollowingSolidus, false); err != nil {
return nil, err
}
}
case StateAuthority:
if r == '@' {
if err := p.handleError(url, errors.InvalidCredentials, false); err != nil {
return nil, err
}
if atFlag {
// Prepend %40 to buffer
tmp := buffer.String()
buffer.Reset()
buffer.WriteString("%40")
buffer.WriteString(tmp)
}
atFlag = true
bb := newInputString(buffer.String())
c := bb.nextCodePoint()
for !bb.eof {
if c == ':' && !passwordTokenSeenFlag {
passwordTokenSeenFlag = true
c = bb.nextCodePoint()
continue
}
encodedCodePoints := p.percentEncodeRune(c, UserInfoPercentEncodeSet)
if passwordTokenSeenFlag {
url.password += encodedCodePoints
} else {
url.username += encodedCodePoints
}
c = bb.nextCodePoint()
}
buffer.Reset()
} else if (input.eof || r == '/' || r == '?' || r == '#') || url.isSpecialSchemeAndBackslash(r) {
if atFlag && buffer.Len() == 0 {
if err := p.handleError(url, errors.InvalidCredentials, true); err != nil {
return nil, err
}
}
input.rewind(len([]rune(buffer.String())) + 1)
buffer.Reset()
state = StateHost
} else {
buffer.WriteRune(r)
}
case StateHost:
fallthrough
case StateHostname:
if stateOverridden && url.scheme == "file" {
input.rewindLast()
state = StateFileHost
} else if r == ':' && !bracketFlag {
if buffer.Len() == 0 {
if err := p.handleError(url, errors.HostMissing, true); err != nil {
return nil, err
}
}
if stateOverride == StateHostname {
return url, nil
}
host, err := p.parseHost(url, p, buffer.String(), !url.IsSpecialScheme())
if err != nil {
return url, err
}
url.host = &host
buffer.Reset()
state = StatePort
} else if input.eof || (r == '/' || r == '?' || r == '#' || url.isSpecialSchemeAndBackslash(r)) {
input.rewindLast()
if url.IsSpecialScheme() && buffer.Len() == 0 {
if err := p.handleError(url, errors.HostMissing, true); err != nil {
return nil, err
}
} else if stateOverridden && buffer.Len() == 0 && (url.username != "" || url.password != "" || url.port != nil) {
return url, nil
} else {
host, err := p.parseHost(url, p, buffer.String(), !url.IsSpecialScheme())
if err != nil {
return url, err
}
url.host = &host
buffer.Reset()
state = StatePathStart
if stateOverridden {
return url, nil
}
}
} else {
if r == '[' {
bracketFlag = true
} else if r == ']' {
bracketFlag = false
}
if input.currentIsInvalid() && p.opts.acceptInvalidCodepoints {
buffer.WriteString(string([]byte{input.getCurrentAsByte()}))
} else {
buffer.WriteRune(r)
}
}
case StatePort:
if ASCIIDigit.Test(uint(r)) {
buffer.WriteRune(r)
} else if (input.eof || r == '/' || r == '?' || r == '#') || url.isSpecialSchemeAndBackslash(r) || stateOverridden {
if buffer.Len() > 0 {
port, err := strconv.Atoi(buffer.String())
if port > 65535 || goerrors.Is(err, strconv.ErrRange) {
if err := p.handleWrappedError(url, errors.PortOutOfRange, true, err); err != nil {
return nil, err
}
}
portString := strconv.Itoa(port)
url.decodedPort = port
url.port = &portString
url.cleanDefaultPort()
buffer.Reset()
}
if stateOverridden {
return url, nil
}
state = StatePathStart
input.rewindLast()
} else {
if err := p.handleError(url, errors.PortInvalid, true); err != nil {
return nil, err
}
}
case StateFile:
url.scheme = "file"
url.host = new(string)
if r == '/' || r == '\\' {
if r == '\\' {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
}
state = StateFileSlash
} else if base != nil && base.scheme == "file" {
url.host = base.host
url.path = base.path
url.query = base.query
if r == '?' {
url.query = new(string)
state = StateQuery
} else if r == '#' {
url.fragment = new(string)
state = StateFragment
} else if !input.eof {
url.query = nil
if !startsWithAWindowsDriveLetter(input.remainingFromPointer()) {
url.path.shortenPath(url.scheme)
} else {
if err := p.handleError(url, errors.FileInvalidWindowsDriveLetter, false); err != nil {
return nil, err
}
url.path.init()
}
state = StatePath
input.rewindLast()
}
} else {
state = StatePath
input.rewindLast()
}
case StateFileSlash:
if r == '/' || r == '\\' {
if r == '\\' {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
}
state = StateFileHost
} else {
if base != nil && base.scheme == "file" {
url.host = base.host
if !startsWithAWindowsDriveLetter(input.remainingFromPointer()) && base.path != nil && isNormalizedWindowsDriveLetter(base.path.p[0]) {
// This is a (platform-independent) Windows drive letter quirk. Both urls and bases host are null under these conditions and therefore not copied
url.path.addSegment(base.path.p[0])
}
}
state = StatePath
input.rewindLast()
}
case StateFileHost:
if input.eof || r == '/' || r == '\\' || r == '?' || r == '#' {
input.rewindLast()
if !stateOverridden && isWindowsDriveLetter(buffer.String()) {
if err := p.handleError(url, errors.FileInvalidWindowsDriveLetterHost, false); err != nil {
return nil, err
}
state = StatePath
} else if buffer.Len() == 0 {
url.host = new(string)
if stateOverridden {
return nil, nil
}
state = StatePathStart
} else {
host, err := p.parseHost(url, p, buffer.String(), !url.IsSpecialScheme())
if err != nil {
return url, err
}
if host == "localhost" {
host = ""
}
url.host = &host
if stateOverridden {
return url, nil
}
buffer.Reset()
state = StatePathStart
}
} else {
buffer.WriteRune(r)
}
case StatePathStart:
if url.IsSpecialScheme() && !p.opts.skipTrailingSlashNormalization {
if r == '\\' {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
}
state = StatePath
if r != '/' && r != '\\' {
input.rewindLast()
}
} else if !stateOverridden && r == '?' {
url.query = new(string)
state = StateQuery
} else if !stateOverridden && r == '#' {
url.fragment = new(string)
state = StateFragment
} else if !input.eof {
state = StatePath
if r != '/' {
input.rewindLast()
}
} else if stateOverridden && url.host == nil {
url.path.addSegment("")
}
case StatePath:
if (input.eof || r == '/') ||
url.isSpecialSchemeAndBackslash(r) ||
(!stateOverridden && (r == '?' || r == '#')) {
if url.isSpecialSchemeAndBackslash(r) {
if err := p.handleError(url, errors.InvalidReverseSolidus, false); err != nil {
return nil, err
}
}
if isDoubleDotPathSegment(buffer.String()) {
url.path.shortenPath(url.scheme)
if r != '/' && !url.isSpecialSchemeAndBackslash(r) {
url.path.addSegment("")
}
} else if isSingleDotPathSegment(buffer.String()) && r != '/' && !url.isSpecialSchemeAndBackslash(r) {
url.path.addSegment("")
} else if !isSingleDotPathSegment(buffer.String()) {
if url.scheme == "file" && url.path.isEmpty() && isWindowsDriveLetter(buffer.String()) {
// replace second code point in buffer with U+003A (:).
// This is a (platform-independent) Windows drive letter quirk.
if !p.opts.skipWindowsDriveLetterNormalization {
b := buffer.String()
buffer.Reset()
buffer.WriteString(b[0:1] + ":" + b[2:])
}
}
if !p.opts.collapseConsecutiveSlashes || !url.IsSpecialScheme() || url.path.isEmpty() || len(url.path.p[len(url.path.p)-1]) > 0 {
url.path.addSegment(buffer.String())
} else {
url.path.p[len(url.path.p)-1] = buffer.String()
}
}
buffer.Reset()
if r == '?' {
url.query = new(string)
state = StateQuery
} else if r == '#' {
url.fragment = new(string)
state = StateFragment
}
} else {
if !isURLCodePoint(r) && r != '%' {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
}
invalidPercentEncoding, d := input.remainingIsInvalidPercentEncoded()
if invalidPercentEncoding {
if err := p.handleErrorWithDescription(url, errors.InvalidURLUnit, false, d); err != nil {
return nil, err
}
}
if invalidPercentEncoding {
buffer.WriteString(p.percentEncodeInvalidRune(r, p.opts.pathPercentEncodeSet))
} else {
buffer.WriteString(p.percentEncodeRune(r, p.opts.pathPercentEncodeSet))
}
}
case StateOpaquePath:
if r == '?' {
url.query = new(string)
state = StateQuery
buffer.Reset()
} else if r == '#' {
url.fragment = new(string)
state = StateFragment
buffer.Reset()
} else if !input.eof {
if !isURLCodePoint(r) && r != '%' {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
}
invalidPercentEncoding, d := input.remainingIsInvalidPercentEncoded()
if invalidPercentEncoding {
if err := p.handleErrorWithDescription(url, errors.InvalidURLUnit, false, d); err != nil {
return nil, err
}
buffer.WriteString(p.percentEncodeInvalidRune(r, C0PercentEncodeSet))
} else {
buffer.WriteString(p.percentEncodeRune(r, C0PercentEncodeSet))
}
url.path.setOpaque(buffer.String())
}
case StateQuery:
if !stateOverridden && r == '#' {
url.fragment = new(string)
state = StateFragment
*url.query = buffer.String()
buffer.Reset()
} else if !input.eof {
if !isURLCodePoint(r) && r != '%' {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
}
invalidPercentEncoding, d := input.remainingIsInvalidPercentEncoded()
if invalidPercentEncoding {
if err := p.handleErrorWithDescription(url, errors.InvalidURLUnit, false, d); err != nil {
return nil, err
}
}
encodeSet := p.opts.queryPercentEncodeSet
if url.isSpecialScheme(url.scheme) {
encodeSet = p.opts.specialQueryPercentEncodeSet
}
buffer.WriteString(p.percentEncodeRune(r, encodeSet))
} else {
q := buffer.String()
url.query = &q
}
case StateFragment:
if !input.eof {
if !isURLCodePoint(r) && r != '%' {
if err := p.handleError(url, errors.InvalidURLUnit, false); err != nil {
return nil, err
}
}
invalidPercentEncoding, d := input.remainingIsInvalidPercentEncoded()
if invalidPercentEncoding {
if err := p.handleErrorWithDescription(url, errors.InvalidURLUnit, false, d); err != nil {
return nil, err
}
}
encodeSet := p.opts.fragmentPercentEncodeSet
if url.isSpecialScheme(url.scheme) {
encodeSet = p.opts.specialFragmentPercentEncodeSet
}
buffer.WriteString(p.percentEncodeRune(r, encodeSet))
} else {
f := buffer.String()
url.fragment = &f
}
}
if input.eof {
break
}
}
return url, nil
}
func (p *parser) percentEncodeInvalidRune(r rune, tr *PercentEncodeSet) string {
if p.opts.percentEncodeSinglePercentSign {
return p.percentEncodeRune(r, tr.Set(0x25))
}
return p.percentEncodeRune(r, tr)
}
func (p *parser) percentEncodeRune(r rune, tr *PercentEncodeSet) string {
if tr != nil && !tr.RuneShouldBeEncoded(r) {
return string(r)
}
var bytes = make([]byte, 4)
var n int
if p.opts.encodingOverride != nil {
b, _ := p.opts.encodingOverride.EncodeRune(r)
bytes[0] = b
n = 1
} else {
n = utf8.EncodeRune(bytes, r)
}
percentEncoded := make([]byte, 4*3)
j := 0
for i := 0; i < n; i++ {
c := bytes[i]
percentEncoded[j] = '%'
percentEncoded[j+1] = "0123456789ABCDEF"[c>>4]
percentEncoded[j+2] = "0123456789ABCDEF"[c&15]
j += 3
}
return string(percentEncoded[:j])
}
func (p *parser) PercentEncodeString(s string, tr *PercentEncodeSet) string {
buffer := &strings.Builder{}
runes := []rune(s)
for i, r := range runes {
if r == '%' {
if len(runes) < (i+3) ||
(!ASCIIHexDigit.Test(uint(runes[i+1])) || !ASCIIHexDigit.Test(uint(runes[i+2]))) {
if p.opts.percentEncodeSinglePercentSign {
buffer.WriteString(p.percentEncodeRune(r, tr.Set(0x25)))
continue
}
}
}
buffer.WriteString(p.percentEncodeRune(r, tr))
}
return buffer.String()
}
func (p *parser) DecodePercentEncoded(s string) string {
sb := strings.Builder{}
bytes := []byte(s)
for i := 0; i < len(bytes); i++ {
if bytes[i] != '%' {
sb.WriteByte(bytes[i])
} else if len(bytes) < (i+3) ||
(!ASCIIHexDigit.Test(uint(bytes[i+1])) || !ASCIIHexDigit.Test(uint(bytes[i+2]))) {
sb.WriteByte(bytes[i])
} else {
b, e := u2.PathUnescape(string(bytes[i : i+3]))
if e != nil {
return sb.String()
}
if p.opts.encodingOverride != nil {
r := p.opts.encodingOverride.DecodeByte(b[0])
sb.WriteRune(r)
} else {
sb.WriteString(b)
}
i += 2
}
}
return sb.String()
}
func (p *parser) NewUrl() *Url {
u := Url{}
u.parser = p
u.path = &path{}
u.path.init()
return &u
}
func isSingleDotPathSegment(s string) bool {
if s == "." {
return true
}
s = strings.ToLower(s)
return s == "%2e"
}
func isDoubleDotPathSegment(s string) bool {
if s == ".." {
return true
}
s = strings.ToLower(s)
if s == ".%2e" || s == "%2e." || s == "%2e%2e" {
return true
}
return false
}
func startsWithAWindowsDriveLetter(s string) bool {
if len(s) >= 2 && isWindowsDriveLetter(s[0:2]) &&
(len(s) == 2 || s[2] == '/' || s[2] == '\\' || s[2] == '?' || s[2] == '#') {
return true
}
return false
}
func isWindowsDriveLetter(s string) bool {
if len(s) == 2 && ASCIIAlpha.Test(uint(s[0])) &&
(s[1] == ':' || s[1] == '|') {
return true
}
return false
}
func isNormalizedWindowsDriveLetter(s string) bool {
if len(s) == 2 && ASCIIAlpha.Test(uint(s[0])) &&
(s[1] == ':') {
return true
}
return false
}
func trimPrefix(s string, tr *PercentEncodeSet) (string, bool) {
if s == "" {
return s, false
}
for i, c := range s {
if tr.RuneNotInSet(c) {
return s[i:], i > 0
}
}
return "", true
}
func trimPostfix(s string, tr *PercentEncodeSet) (string, bool) {
if s == "" {
return s, false
}
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
if tr.RuneNotInSet(int32(c)) {
return s[:i+1], i < (len(s) - 1)
}
}
return "", true
}
func trim(s string, tr *PercentEncodeSet) (string, bool) {
var c1, c2 bool
s, c1 = trimPrefix(s, tr)
s, c2 = trimPostfix(s, tr)
return s, c1 || c2
}
func remove(s string, tr *bitset.BitSet) (string, bool) {
if s == "" {
return s, false
}
changed := false
var r []byte
for _, c := range []byte(s) {
if tr.Test(uint(c)) {
changed = true
} else {
r = append(r, c)
}
}
return string(r), changed
}
func containsOnly(s string, tr *bitset.BitSet) bool {
for _, c := range []byte(s) {
if !tr.Test(uint(c)) {
return false
}
}
return true
}
func (u *Url) IsSpecialScheme() bool {
return u.isSpecialScheme(u.scheme)
}
func (u *Url) isSpecialScheme(s string) bool {
_, ok := u.getSpecialScheme(s)
return ok
}
func (u *Url) getSpecialScheme(s string) (string, bool) {
dp, ok := u.parser.opts.specialSchemes[s]
return dp, ok
}
func (u *Url) isSpecialSchemeAndBackslash(r rune) bool {
ok := u.IsSpecialScheme()
return ok && r == '\\'
}
func (u *Url) cleanDefaultPort() {
if dp, ok := u.getSpecialScheme(u.scheme); ok && (u.port == nil || dp == *u.port) {
u.port = nil
u.decodedPort = 0
}
}
func (u *Url) getDefaultPort() int {
if dp, ok := u.getSpecialScheme(u.scheme); ok {
if p, err := strconv.Atoi(dp); err == nil {
return p
}
}
return 0
}