module Strings
export HTTPVersion, escapehtml, tocameldash, iso8859_1_to_utf8, ascii_lc_isequal
using ..IOExtras
# A `Base.VersionNumber` is a SemVer spec, whereas a HTTP versions is just 2 digits,
# This allows us to use a smaller type and more importantly write a simple parse method
# that avoid allocations.
"""
HTTPVersion(major, minor)
The HTTP version number consists of two digits separated by a
"." (period or decimal point). The first digit (`major` version)
indicates the HTTP messaging syntax, whereas the second digit (`minor`
version) indicates the highest minor version within that major
version to which the sender is conformant and able to understand for
future communication.
See [RFC7230 2.6](https://tools.ietf.org/html/rfc7230#section-2.6)
"""
struct HTTPVersion
major::UInt8
minor::UInt8
end
HTTPVersion(major::Integer) = HTTPVersion(major, 0x00)
HTTPVersion(v::AbstractString) = parse(HTTPVersion, v)
HTTPVersion(v::VersionNumber) = convert(HTTPVersion, v)
# Lossy conversion. We ignore patch/prerelease/build parts even if non-zero/non-empty,
# because we don't want to add overhead for a case that should never be relevant.
Base.convert(::Type{HTTPVersion}, v::VersionNumber) = HTTPVersion(v.major, v.minor)
Base.VersionNumber(v::HTTPVersion) = VersionNumber(v.major, v.minor)
Base.show(io::IO, v::HTTPVersion) = print(io, "HTTPVersion(\"", string(v.major), ".", string(v.minor), "\")")
Base.:(==)(va::VersionNumber, vb::HTTPVersion) = va == VersionNumber(vb)
Base.:(==)(va::HTTPVersion, vb::VersionNumber) = VersionNumber(va) == vb
Base.isless(va::VersionNumber, vb::HTTPVersion) = isless(va, VersionNumber(vb))
Base.isless(va::HTTPVersion, vb::VersionNumber) = isless(VersionNumber(va), vb)
function Base.isless(va::HTTPVersion, vb::HTTPVersion)
va.major < vb.major && return true
va.major > vb.major && return false
va.minor < vb.minor && return true
return false
end
function Base.parse(::Type{HTTPVersion}, v::AbstractString)
ver = tryparse(HTTPVersion, v)
ver === nothing && throw(ArgumentError("invalid HTTP version string: $(repr(v))"))
return ver
end
# We only support single-digits for major and minor versions
# - we can parse 0.9 but not 0.10
# - we can parse 9.0 but not 10.0
function Base.tryparse(::Type{HTTPVersion}, v::AbstractString)
isempty(v) && return nothing
len = ncodeunits(v)
i = firstindex(v)
d1 = v[i]
if isdigit(d1)
major = parse(UInt8, d1)
else
return nothing
end
i = nextind(v, i)
i > len && return HTTPVersion(major)
dot = v[i]
dot == '.' || return nothing
i = nextind(v, i)
i > len && return HTTPVersion(major)
d2 = v[i]
if isdigit(d2)
minor = parse(UInt8, d2)
else
return nothing
end
return HTTPVersion(major, minor)
end
"""
escapehtml(i::String)
Returns a string with special HTML characters escaped: &, <, >, ", '
"""
function escapehtml(i::AbstractString)
# Refer to http://stackoverflow.com/a/7382028/3822752 for spec. links
o = replace(i, "&" =>"&")
o = replace(o, "\""=>""")
o = replace(o, "'" =>"'")
o = replace(o, "<" =>"<")
o = replace(o, ">" =>">")
return o
end
"""
tocameldash(s::String)
Ensure the first character and characters that follow a '-' are uppercase.
"""
function tocameldash(s::String)
toUpper = UInt8('A') - UInt8('a')
v = Vector{UInt8}(bytes(s))
upper = true
for i = 1:length(v)
@inbounds b = v[i]
if upper
islower(b) && (v[i] = b + toUpper)
else
isupper(b) && (v[i] = lower(b))
end
upper = b == UInt8('-')
end
return String(v)
end
tocameldash(s::AbstractString) = tocameldash(String(s))
@inline islower(b::UInt8) = UInt8('a') <= b <= UInt8('z')
@inline isupper(b::UInt8) = UInt8('A') <= b <= UInt8('Z')
@inline lower(c::UInt8) = c | 0x20
"""
iso8859_1_to_utf8(bytes::AbstractVector{UInt8})
Convert from ISO8859_1 to UTF8.
"""
function iso8859_1_to_utf8(bytes::AbstractVector{UInt8})
io = IOBuffer()
for b in bytes
if b < 0x80
write(io, b)
else
write(io, 0xc0 | (b >> 6))
write(io, 0x80 | (b & 0x3f))
end
end
return String(take!(io))
end
"""
Convert ASCII (RFC20) character `c` to lower case.
"""
ascii_lc(c::UInt8) = c in UInt8('A'):UInt8('Z') ? c + 0x20 : c
"""
Case insensitive ASCII character comparison.
"""
ascii_lc_isequal(a::UInt8, b::UInt8) = ascii_lc(a) == ascii_lc(b)
"""
HTTP.ascii_lc_isequal(a::String, b::String)
Case insensitive ASCII string comparison.
"""
function ascii_lc_isequal(a, b)
acu = codeunits(a)
bcu = codeunits(b)
len = length(acu)
len != length(bcu) && return false
for i = 1:len
@inbounds !ascii_lc_isequal(acu[i], bcu[i]) && return false
end
return true
end
end # module Strings