419 lines
9.7 KiB
Lua
419 lines
9.7 KiB
Lua
local new_tab = require "table.new"
|
|
|
|
|
|
local type = type
|
|
local ipairs = ipairs
|
|
local tostring = tostring
|
|
local lower = string.lower
|
|
local sub = string.sub
|
|
local fmt = string.format
|
|
local find = string.find
|
|
local gsub = string.gsub
|
|
local byte = string.byte
|
|
local huge = math.huge
|
|
|
|
|
|
local SPACE_BYTE = byte(" ")
|
|
local TAB_BYTE = byte("\t")
|
|
local CR_BYTE = byte("\r")
|
|
|
|
|
|
local _M = {}
|
|
|
|
|
|
_M.join = require("pl.stringx").join
|
|
|
|
|
|
--- splits a string (kept for backward compatibility, use splitn instead).
|
|
-- just a placeholder to the penlight `pl.stringx.split` function
|
|
-- @function split
|
|
_M.split = require("pl.stringx").split
|
|
|
|
|
|
local function split_once_common(value, pattern, plain)
|
|
if value == nil then
|
|
return nil, nil
|
|
elseif pattern == nil then
|
|
return value, nil
|
|
elseif pattern == "" then
|
|
return "", value
|
|
elseif value == "" then
|
|
return "", nil
|
|
end
|
|
|
|
local s, e = find(value, pattern, nil, plain)
|
|
if not s then
|
|
return value, nil
|
|
end
|
|
|
|
if s == 1 and e == 1 then
|
|
return "", (sub(value, e + 1))
|
|
end
|
|
|
|
return (sub(value, 1, s - 1)), (sub(value, e + 1))
|
|
end
|
|
|
|
|
|
--- splits a string once with a plain delimiter.
|
|
-- @function split_once
|
|
function _M.split_once(value, delim)
|
|
return split_once_common(value, delim, true)
|
|
end
|
|
|
|
|
|
--- splits a string once with a pattern.
|
|
-- @function split_once
|
|
function _M.psplit_once(value, pattern)
|
|
return split_once_common(value, pattern, false)
|
|
end
|
|
|
|
|
|
-- This code is optimized, you can find microbenchmarks in:
|
|
-- https://github.com/Kong/kong/pull/14388
|
|
--
|
|
-- Note that the results may also vary a bit on different architectures.
|
|
--
|
|
-- Here is a small part of that:
|
|
-- local monotonic_msec = require("resty.core.time").monotonic_msec
|
|
-- local update_time = ngx.update_time
|
|
-- local splitn = require("kong.tools.string").splitn
|
|
-- local TEST_SET = {
|
|
-- "",
|
|
-- ",",
|
|
-- ",,",
|
|
-- "a,,b",
|
|
-- "a,b,c",
|
|
-- "aaa bbb,cccc dddd,eeeeee,ffff,,ggg,hhhhhhh,placeholderplaceholderplaceholderplaceholderplaceholderplaceholderplaceholder,ii",
|
|
-- "aaa bbb,cccc dddd,eeeeee,ffff,,ggg,hhhhhhh,placeholderplaceholderplaceholderplaceholderplaceholderplaceholderplaceholder,ii, jjj",
|
|
-- }
|
|
-- local TEST_COUNT = #TEST_SET
|
|
-- local ITERATIONS = 1000000
|
|
-- update_time()
|
|
-- local s = monotonic_msec()
|
|
-- for _ = 1, ITERATIONS do
|
|
-- for i = 1, TEST_COUNT do
|
|
-- local _ = splitn(TEST_SET[i], ",")
|
|
-- end
|
|
-- end
|
|
-- update_time()
|
|
-- local e = monotonic_msec()
|
|
-- print("took: ", e - s, " ms")
|
|
local function splitn_common(value, pattern, n, plain)
|
|
local limit = n or huge
|
|
if limit < 1 or value == nil then
|
|
return {}, 0
|
|
|
|
elseif limit == 1 or pattern == nil then
|
|
return { value }, 1
|
|
|
|
elseif pattern == "" then
|
|
if value == "" then
|
|
return { "", "" }, 2
|
|
end
|
|
|
|
local size = #value
|
|
if size == 1 then
|
|
if limit == 2 then
|
|
return { "", value }, 2
|
|
else
|
|
return { "", value, "" }, 3
|
|
end
|
|
end
|
|
|
|
size = limit >= size + 2 and size + 2 or limit
|
|
local t = new_tab(size, 0)
|
|
t[1] = ""
|
|
for i = 2, size do
|
|
t[i] = sub(value, i - 1, i < size and i - 1 or nil)
|
|
end
|
|
return t, size
|
|
|
|
elseif value == "" then
|
|
return { "" }, 1
|
|
end
|
|
|
|
local p = 1
|
|
local i = 1
|
|
local t = new_tab(n or 10, 0)
|
|
::again::
|
|
if i < limit then
|
|
local s, e = find(value, pattern, p, plain)
|
|
if s then
|
|
t[i] = sub(value, p, s - 1)
|
|
i = i + 1
|
|
p = e + 1
|
|
goto again
|
|
end
|
|
end
|
|
t[i] = sub(value, p)
|
|
return t, i
|
|
end
|
|
|
|
|
|
local function noop_iter() end
|
|
local function once_iter(invariant, control)
|
|
return invariant ~= control and invariant or nil
|
|
end
|
|
local function split_iter(t)
|
|
local i = t[0] or 1
|
|
t[0] = i + 1
|
|
return t[i]
|
|
end
|
|
|
|
|
|
--- splits a string with a plain delimiter (much faster than the split above).
|
|
-- @function splitn
|
|
function _M.splitn(value, delim, n)
|
|
return splitn_common(value, delim, n, true)
|
|
end
|
|
|
|
|
|
--- splits a string with a pattern.
|
|
-- @function psplitn
|
|
function _M.psplitn(value, pattern, n)
|
|
return splitn_common(value, pattern, n, false)
|
|
end
|
|
|
|
|
|
--- string splitting iterator (plain delimiter).
|
|
-- @function isplitn
|
|
function _M.isplitn(value, delim, n)
|
|
local res, count = splitn_common(value, delim, n, true)
|
|
if count == 0 then
|
|
return noop_iter
|
|
elseif count == 1 then
|
|
return once_iter, res[1]
|
|
end
|
|
return split_iter, res
|
|
end
|
|
|
|
|
|
--- string splitting iterator (pattern delimiter).
|
|
-- @function ipsplitn
|
|
function _M.ipsplitn(value, pattern, n)
|
|
local res, count = splitn_common(value, pattern, n, false)
|
|
if count == 0 then
|
|
return noop_iter
|
|
elseif count == 1 then
|
|
return once_iter, res[1]
|
|
end
|
|
return split_iter, res
|
|
end
|
|
|
|
|
|
--- strips whitespace from a string.
|
|
-- @function strip
|
|
function _M.strip(value)
|
|
if value == nil then
|
|
return ""
|
|
end
|
|
|
|
-- TODO: do we want to operate on non-string values (kept for backward compatibility)?
|
|
if type(value) ~= "string" then
|
|
value = tostring(value) or ""
|
|
end
|
|
|
|
if value == "" then
|
|
return ""
|
|
end
|
|
|
|
local s = 1 -- position of the leftmost non-whitespace char
|
|
::spos::
|
|
local b = byte(value, s)
|
|
if not b then -- reached the end of the all whitespace string
|
|
return ""
|
|
end
|
|
if b == SPACE_BYTE or (b >= TAB_BYTE and b <= CR_BYTE) then
|
|
s = s + 1
|
|
goto spos
|
|
end
|
|
|
|
local e = -1 -- position of the rightmost non-whitespace char
|
|
::epos::
|
|
b = byte(value, e)
|
|
if b == SPACE_BYTE or (b >= TAB_BYTE and b <= CR_BYTE) then
|
|
e = e - 1
|
|
goto epos
|
|
end
|
|
|
|
if s ~= 1 or e ~= -1 then
|
|
value = sub(value, s, e)
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
|
|
-- Numbers taken from table 3-7 in www.unicode.org/versions/Unicode6.2.0/UnicodeStandard-6.2.pdf
|
|
-- find-based solution inspired by http://notebook.kulchenko.com/programming/fixing-malformed-utf8-in-lua
|
|
function _M.validate_utf8(val)
|
|
local str = tostring(val)
|
|
local i, len = 1, #str
|
|
while i <= len do
|
|
if i == find(str, "[%z\1-\127]", i) then i = i + 1
|
|
elseif i == find(str, "[\194-\223][\123-\191]", i) then i = i + 2
|
|
elseif i == find(str, "\224[\160-\191][\128-\191]", i)
|
|
or i == find(str, "[\225-\236][\128-\191][\128-\191]", i)
|
|
or i == find(str, "\237[\128-\159][\128-\191]", i)
|
|
or i == find(str, "[\238-\239][\128-\191][\128-\191]", i) then i = i + 3
|
|
elseif i == find(str, "\240[\144-\191][\128-\191][\128-\191]", i)
|
|
or i == find(str, "[\241-\243][\128-\191][\128-\191][\128-\191]", i)
|
|
or i == find(str, "\244[\128-\143][\128-\191][\128-\191]", i) then i = i + 4
|
|
else
|
|
return false, i
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Converts bytes to another unit in a human-readable string.
|
|
-- @tparam number bytes A value in bytes.
|
|
--
|
|
-- @tparam[opt] string unit The unit to convert the bytes into. Can be either
|
|
-- of `b/B`, `k/K`, `m/M`, or `g/G` for bytes (unchanged), kibibytes,
|
|
-- mebibytes, or gibibytes, respectively. Defaults to `b` (bytes).
|
|
-- @tparam[opt] number scale The number of digits to the right of the decimal
|
|
-- point. Defaults to 2.
|
|
-- @treturn string A human-readable string.
|
|
-- @usage
|
|
--
|
|
-- bytes_to_str(5497558) -- "5497558"
|
|
-- bytes_to_str(5497558, "m") -- "5.24 MiB"
|
|
-- bytes_to_str(5497558, "G", 3) -- "5.120 GiB"
|
|
--
|
|
function _M.bytes_to_str(bytes, unit, scale)
|
|
local u = lower(unit or "")
|
|
|
|
if u == "" or u == "b" then
|
|
return fmt("%d", bytes)
|
|
end
|
|
|
|
scale = scale or 2
|
|
|
|
if type(scale) ~= "number" or scale < 0 then
|
|
error("scale must be equal or greater than 0", 2)
|
|
end
|
|
|
|
local fspec = fmt("%%.%df", scale)
|
|
|
|
if u == "k" then
|
|
return fmt(fspec .. " KiB", bytes / 2^10)
|
|
end
|
|
|
|
if u == "m" then
|
|
return fmt(fspec .. " MiB", bytes / 2^20)
|
|
end
|
|
|
|
if u == "g" then
|
|
return fmt(fspec .. " GiB", bytes / 2^30)
|
|
end
|
|
|
|
error("invalid unit '" .. unit .. "' (expected 'k/K', 'm/M', or 'g/G')", 2)
|
|
end
|
|
|
|
|
|
local SCALES = {
|
|
k = 1024,
|
|
K = 1024,
|
|
m = 1024 * 1024,
|
|
M = 1024 * 1024,
|
|
g = 1024 * 1024 * 1024,
|
|
G = 1024 * 1024 * 1024,
|
|
}
|
|
|
|
function _M.parse_ngx_size(str)
|
|
assert(type(str) == "string", "Parameter #1 must be a string")
|
|
|
|
local len = #str
|
|
local unit = sub(str, len)
|
|
local scale = SCALES[unit]
|
|
|
|
if scale then
|
|
len = len - 1
|
|
|
|
else
|
|
scale = 1
|
|
end
|
|
|
|
local size = tonumber(sub(str, 1, len)) or 0
|
|
|
|
return size * scale
|
|
end
|
|
|
|
|
|
local try_decode_base64
|
|
do
|
|
local decode_base64 = ngx.decode_base64
|
|
local decode_base64url = require "ngx.base64".decode_base64url
|
|
|
|
local function decode_base64_str(str)
|
|
if type(str) == "string" then
|
|
return decode_base64(str)
|
|
or decode_base64url(str)
|
|
or nil, "base64 decoding failed: invalid input"
|
|
|
|
else
|
|
return nil, "base64 decoding failed: not a string"
|
|
end
|
|
end
|
|
|
|
function try_decode_base64(value)
|
|
if type(value) == "table" then
|
|
for i, v in ipairs(value) do
|
|
value[i] = decode_base64_str(v) or v
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
if type(value) == "string" then
|
|
return decode_base64_str(value) or value
|
|
end
|
|
|
|
return value
|
|
end
|
|
end
|
|
_M.try_decode_base64 = try_decode_base64
|
|
|
|
|
|
local replace_dashes
|
|
local replace_dashes_lower
|
|
do
|
|
local str_replace_char
|
|
|
|
if ngx and ngx.config.subsystem == "http" then
|
|
|
|
-- 1,000,000 iterations with input of "my-header":
|
|
-- string.gsub: 81ms
|
|
-- ngx.re.gsub: 74ms
|
|
-- loop/string.buffer: 28ms
|
|
-- str_replace_char: 14ms
|
|
str_replace_char = require("resty.core.utils").str_replace_char
|
|
|
|
else -- stream subsystem
|
|
str_replace_char = function(str, ch, replace)
|
|
if not find(str, ch, nil, true) then
|
|
return str
|
|
end
|
|
|
|
return gsub(str, ch, replace)
|
|
end
|
|
end
|
|
|
|
replace_dashes = function(str)
|
|
return str_replace_char(str, "-", "_")
|
|
end
|
|
|
|
replace_dashes_lower = function(str)
|
|
return str_replace_char(str:lower(), "-", "_")
|
|
end
|
|
end
|
|
_M.replace_dashes = replace_dashes
|
|
_M.replace_dashes_lower = replace_dashes_lower
|
|
|
|
|
|
return _M
|