diff --git a/framework/lualib/3rd/lpeg_patterns/IPv4.lua b/framework/lualib/3rd/lpeg_patterns/IPv4.lua new file mode 100755 index 0000000..e553040 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/IPv4.lua @@ -0,0 +1,45 @@ +-- IPv4 + +local lpeg = require "lpeg" +local P = lpeg.P +local R = lpeg.R +local Cg = lpeg.Cg + +local core = require "lpeg_patterns.core" +local DIGIT = core.DIGIT + +local dec_octet = ( + P"1" * DIGIT * DIGIT + + P"2" * (R"04"*DIGIT + P"5"*R"05") + + DIGIT * DIGIT^-1 + ) / tonumber + +local IPv4_methods = {} +local IPv4_mt = { + __name = "lpeg_patterns.IPv4"; + __index = IPv4_methods; +} + +local function new_IPv4 ( o1 , o2 , o3 , o4 ) + return setmetatable({o1, o2, o3, o4}, IPv4_mt) +end + +function IPv4_methods:unpack() + return self[1], self[2], self[3], self[4] +end + +function IPv4_methods:binary() + return string.char(self:unpack()) +end + +function IPv4_mt:__tostring ( ) + return string.format("%d.%d.%d.%d", self:unpack()) +end + +local IPv4address = Cg ( dec_octet * P"." * dec_octet * P"." * dec_octet * P"." * dec_octet ) / new_IPv4 + +return { + IPv4_methods = IPv4_methods; + IPv4_mt = IPv4_mt; + IPv4address = IPv4address; +} diff --git a/framework/lualib/3rd/lpeg_patterns/IPv6.lua b/framework/lualib/3rd/lpeg_patterns/IPv6.lua new file mode 100755 index 0000000..b4ab116 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/IPv6.lua @@ -0,0 +1,144 @@ +-- IPv6 + +local unpack = table.unpack or unpack -- luacheck: ignore 113 143 + +local lpeg = require "lpeg" +local P = lpeg.P +local V = lpeg.V +local Cc = lpeg.Cc +local Cg = lpeg.Cg + +local util = require "lpeg_patterns.util" + +local core = require "lpeg_patterns.core" +local HEXDIG = core.HEXDIG + +local IPv4address = require "lpeg_patterns.IPv4".IPv4address + +local IPv6_methods = {} +local IPv6_mt = { + __name = "lpeg_patterns.IPv6"; + __index = IPv6_methods; +} + +local function new_IPv6(o1, o2, o3, o4, o5, o6, o7, o8, zoneid) + return setmetatable({ + o1, o2, o3, o4, o5, o6, o7, o8, + zoneid = zoneid; + }, IPv6_mt) +end + +function IPv6_methods:unpack() + return self[1], self[2], self[3], self[4], self[5], self[6], self[7], self[8], self.zoneid +end + +function IPv6_methods:binary() + local t = {} + for i=1, 8 do + local lo = self[i] % 256 + t[i*2-1] = (self[i] - lo) / 256 + t[i*2] = lo + end + -- TODO: append zoneid. + -- In a struct sockaddr_in6 it is the numeric index of the scope, so need to lookup? + return string.char(unpack(t, 1, 16)) +end + +function IPv6_methods:setzoneid(zoneid) + self.zoneid = zoneid +end + +function IPv6_mt:__tostring() + local fmt_str + if self.zoneid then + fmt_str = "%x:%x:%x:%x:%x:%x:%x:%x%%%s" + else + fmt_str = "%x:%x:%x:%x:%x:%x:%x:%x" + end + return string.format(fmt_str, self:unpack()) +end + +-- RFC 3986 Section 3.2.2 +-- This is written as a grammar to reduce memory usage +local raw_IPv6address = Cg(P{ + h16 = HEXDIG * HEXDIG^-3 / util.read_hex; + h16c = V"h16" * P":"; + ls32 = ( V"h16c" * V"h16" ) + IPv4address / function ( ipv4 ) + local o1, o2, o3, o4 = ipv4:unpack() + return o1*2^8 + o2 , o3*2^8 + o4 + end; + + mh16c_1 = V"h16c"; + mh16c_2 = V"h16c" * V"h16c"; + mh16c_3 = V"h16c" * V"h16c" * V"h16c"; + mh16c_4 = V"h16c" * V"h16c" * V"h16c" * V"h16c"; + mh16c_5 = V"h16c" * V"h16c" * V"h16c" * V"h16c" * V"h16c"; + mh16c_6 = V"h16c" * V"h16c" * V"h16c" * V"h16c" * V"h16c" * V"h16c"; + + mcc_1 = P"::" * Cc(0); + mcc_2 = P"::" * Cc(0, 0); + mcc_3 = P"::" * Cc(0, 0, 0); + mcc_4 = P"::" * Cc(0, 0, 0, 0); + mcc_5 = P"::" * Cc(0, 0, 0, 0, 0); + mcc_6 = P"::" * Cc(0, 0, 0, 0, 0, 0); + mcc_7 = P"::" * Cc(0, 0, 0, 0, 0, 0, 0); + mcc_8 = P"::" * Cc(0, 0, 0, 0, 0, 0, 0, 0); + + mh16_1 = V"h16"; + mh16_2 = V"mh16c_1" * V"h16"; + mh16_3 = V"mh16c_2" * V"h16"; + mh16_4 = V"mh16c_3" * V"h16"; + mh16_5 = V"mh16c_4" * V"h16"; + mh16_6 = V"mh16c_5" * V"h16"; + mh16_7 = V"mh16c_6" * V"h16"; + + V"mh16c_6" * V"ls32" + + V"mcc_1" * V"mh16c_5" * V"ls32" + + V"mcc_2" * V"mh16c_4" * V"ls32" + + V"h16" * V"mcc_1" * V"mh16c_4" * V"ls32" + + V"mcc_3" * V"mh16c_3" * V"ls32" + + V"h16" * V"mcc_2" * V"mh16c_3" * V"ls32" + + V"mh16_2" * V"mcc_1" * V"mh16c_3" * V"ls32" + + V"mcc_4" * V"mh16c_2" * V"ls32" + + V"h16" * V"mcc_3" * V"mh16c_2" * V"ls32" + + V"mh16_2" * V"mcc_2" * V"mh16c_2" * V"ls32" + + V"mh16_3" * V"mcc_1" * V"mh16c_2" * V"ls32" + + V"mcc_5" * V"h16c" * V"ls32" + + V"h16" * V"mcc_4" * V"h16c" * V"ls32" + + V"mh16_2" * V"mcc_3" * V"h16c" * V"ls32" + + V"mh16_3" * V"mcc_2" * V"h16c" * V"ls32" + + V"mh16_4" * V"mcc_1" * V"h16c" * V"ls32" + + V"mcc_6" * V"ls32" + + V"h16" * V"mcc_5" * V"ls32" + + V"mh16_2" * V"mcc_4" * V"ls32" + + V"mh16_3" * V"mcc_3" * V"ls32" + + V"mh16_4" * V"mcc_2" * V"ls32" + + V"mh16_5" * V"mcc_1" * V"ls32" + + V"mcc_7" * V"h16" + + V"h16" * V"mcc_6" * V"h16" + + V"mh16_2" * V"mcc_5" * V"h16" + + V"mh16_3" * V"mcc_4" * V"h16" + + V"mh16_4" * V"mcc_3" * V"h16" + + V"mh16_5" * V"mcc_2" * V"h16" + + V"mh16_6" * V"mcc_1" * V"h16" + + V"mcc_8" + + V"mh16_1" * V"mcc_7" + + V"mh16_2" * V"mcc_6" + + V"mh16_3" * V"mcc_5" + + V"mh16_4" * V"mcc_4" + + V"mh16_5" * V"mcc_3" + + V"mh16_6" * V"mcc_2" + + V"mh16_7" * V"mcc_1" +}) + +local IPv6address = raw_IPv6address / new_IPv6 + +local ZoneID = P(1)^1 -- ZoneIDs can be any character +local IPv6addrz = raw_IPv6address * (P"%" * ZoneID)^-1 / new_IPv6 + +return { + IPv6_methods = IPv6_methods; + IPv6_mt = IPv6_mt; + IPv6address = IPv6address; + IPv6addrz = IPv6addrz; +} diff --git a/framework/lualib/3rd/lpeg_patterns/core.lua b/framework/lualib/3rd/lpeg_patterns/core.lua new file mode 100755 index 0000000..90cd554 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/core.lua @@ -0,0 +1,30 @@ +-- Core Rules +-- https://tools.ietf.org/html/rfc5234#appendix-B.1 + +local lpeg = require "lpeg" + +local P = lpeg.P +local R = lpeg.R +local S = lpeg.S + +local _M = { } + +_M.ALPHA = R("AZ","az") +_M.BIT = S"01" +_M.CHAR = R"\1\127" +_M.CR = P"\r" +_M.CRLF = P"\r\n" +_M.CTL = R"\0\31" + P"\127" +_M.DIGIT = R"09" +_M.DQUOTE= P'"' +_M.HEXDIG= _M.DIGIT + S"ABCDEFabcdef" +_M.HTAB = P"\t" +_M.LF = P"\n" +_M.OCTET = P(1) +_M.SP = P" " +_M.VCHAR = R"\33\126" +_M.WSP = S" \t" + +_M.LWSP = (_M.WSP + _M.CRLF*_M.WSP)^0 + +return _M diff --git a/framework/lualib/3rd/lpeg_patterns/email.lua b/framework/lualib/3rd/lpeg_patterns/email.lua new file mode 100755 index 0000000..5d9fbe0 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/email.lua @@ -0,0 +1,94 @@ +-- Email Addresses +-- RFC 5322 Section 3.4.1 + +local lpeg = require "lpeg" +local P = lpeg.P +local R = lpeg.R +local S = lpeg.S +local V = lpeg.V +local C = lpeg.C +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local Cs = lpeg.Cs + +local core = require "lpeg_patterns.core" +local CHAR = core.CHAR +local CRLF = core.CRLF +local CTL = core.CTL +local DQUOTE = core.DQUOTE +local WSP = core.WSP +local VCHAR = core.VCHAR + +local obs_NO_WS_CTL = R("\1\8", "\11\12", "\14\31") + P"\127" + +local obs_qp = Cg(P"\\" * C(P"\0" + obs_NO_WS_CTL + core.LF + core.CR)) +local quoted_pair = Cg(P"\\" * C(VCHAR + WSP)) + obs_qp + +-- Folding White Space +local FWS = (WSP^0 * CRLF)^-1 * WSP^1 / " " -- Fold whitespace into a single " " + +-- Comments +local ctext = R"\33\39" + R"\42\91" + R"\93\126" +local comment = P { + V"comment" ; + ccontent = ctext + quoted_pair + V"comment" ; + comment = P"("* (FWS^-1 * V"ccontent")^0 * FWS^-1 * P")"; +} +local CFWS = ((FWS^-1 * comment)^1 * FWS^-1 + FWS ) / function() end + +-- Atom +local specials = S[=[()<>@,;:\".[]]=] +local atext = CHAR-specials-P" "-CTL +local atom = CFWS^-1 * C(atext^1) * CFWS^-1 +local dot_atom_text = C(atext^1 * ( P"." * atext^1 )^0) +local dot_atom = CFWS^-1 * dot_atom_text * CFWS^-1 + +-- Quoted Strings +local qtext = S"\33"+R("\35\91","\93\126") +local qcontent = qtext + quoted_pair +local quoted_string_text = DQUOTE * Cs((FWS^-1 * qcontent)^0 * FWS^-1) * DQUOTE +local quoted_string = CFWS^-1 * quoted_string_text * CFWS^-1 + +-- Miscellaneous Tokens +local word = atom + quoted_string +local obs_phrase = C(word * (word + P"." + CFWS)^0 / function() end) +local phrase = obs_phrase -- obs_phrase is more broad than `word^1`, it's really the same but allows "." + +-- Addr-spec +local obs_dtext = obs_NO_WS_CTL + quoted_pair +local dtext = R("\33\90", "\94\126") + obs_dtext +local domain_literal_text = P"[" * Cs((FWS^-1 * dtext)^0 * FWS^-1) * P"]" + +local domain_text = dot_atom_text + domain_literal_text +local local_part_text = dot_atom_text + quoted_string_text +local addr_spec_text = local_part_text * P"@" * domain_text + +local domain_literal = CFWS^-1 * domain_literal_text * CFWS^-1 +local obs_domain = Ct(atom * (C"." * atom)^0) / table.concat +local domain = obs_domain + dot_atom + domain_literal +local obs_local_part = Ct(word * (C"." * word)^0) / table.concat +local local_part = obs_local_part + dot_atom + quoted_string +local addr_spec = local_part * P"@" * domain + +local display_name = phrase +local obs_domain_list = (CFWS + P",")^0 * P"@" * domain + * (P"," * CFWS^-1 * (P"@" * domain)^-1)^0 +local obs_route = Cg(Ct(obs_domain_list) * P":", "route") +local obs_angle_addr = CFWS^-1 * P"<" * obs_route * addr_spec * P">" * CFWS^-1 +local angle_addr = CFWS^-1 * P"<" * addr_spec * P">" * CFWS^-1 + + obs_angle_addr +local name_addr = Cg(display_name, "display")^-1 * angle_addr +local mailbox = name_addr + addr_spec + +return { + local_part = local_part; + domain = domain; + email = addr_spec; + name_addr = name_addr; + mailbox = mailbox; + + -- A variant that does not allow comments or folding whitespace + local_part_nocfws = local_part_text; + domain_nocfws = domain_text; + email_nocfws = addr_spec_text; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http.lua b/framework/lualib/3rd/lpeg_patterns/http.lua new file mode 100755 index 0000000..4a9e11b --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http.lua @@ -0,0 +1,171 @@ +-- HTTP related patterns + +local _M = {} + +-- RFC 7230 +local http_core = require "lpeg_patterns.http.core" +_M.OWS = http_core.OWS +_M.RWS = http_core.RWS +_M.BWS = http_core.BWS + +_M.chunk_ext = http_core.chunk_ext +_M.comment = http_core.comment +_M.field_name = http_core.field_name +_M.field_value = http_core.field_value +_M.header_field = http_core.header_field +_M.qdtext = http_core.qdtext +_M.quoted_string = http_core.quoted_string +_M.request_line = http_core.request_line +_M.request_target = http_core.request_target +_M.token = http_core.token + +_M.Connection = http_core.Connection +_M.Content_Length = http_core.Content_Length +_M.Host = http_core.Host +_M.TE = http_core.TE +_M.Trailer = http_core.Trailer +_M.Transfer_Encoding = http_core.Transfer_Encoding +_M.Upgrade = http_core.Upgrade +_M.Via = http_core.Via + +-- RFC 7231 +local http_semantics = require "lpeg_patterns.http.semantics" + +_M.IMF_fixdate = http_semantics.IMF_fixdate + +_M.Accept = http_semantics.Accept +_M.Accept_Charset = http_semantics.Accept_Charset +_M.Accept_Encoding = http_semantics.Accept_Encoding +_M.Accept_Language = http_semantics.Accept_Language +_M.Allow = http_semantics.Allow +_M.Content_Encoding = http_semantics.Content_Encoding +_M.Content_Language = http_semantics.Content_Language +_M.Content_Location = http_semantics.Content_Location +_M.Content_Type = http_semantics.Content_Type +_M.Date = http_semantics.Date +_M.Expect = http_semantics.Expect +_M.From = http_semantics.From +_M.Location = http_semantics.Location +_M.Max_Forwards = http_semantics.Max_Forwards +_M.Referer = http_semantics.Referer +_M.Retry_After = http_semantics.Retry_After +_M.Server = http_semantics.Server +_M.User_Agent = http_semantics.User_Agent +_M.Vary = http_semantics.Vary + +-- RFC 7232 +local http_conditional = require "lpeg_patterns.http.conditional" +_M.ETag = http_conditional.ETag +_M.If_Match = http_conditional.If_Match +_M.If_Modified_Since = http_conditional.If_Modified_Since +_M.If_None_Match = http_conditional.If_None_Match +_M.If_Unmodified_Since = http_conditional.If_Unmodified_Since +_M.Last_Modified = http_conditional.Last_Modified + +-- RFC 7233 +local http_range = require "lpeg_patterns.http.range" +_M.Accept_Ranges = http_range.Accept_Ranges +_M.Range = http_range.Range +_M.If_Range = http_range.If_Range +_M.Content_Range = http_range.Content_Range + +-- RFC 7234 +local http_caching = require "lpeg_patterns.http.caching" +_M.Age = http_caching.Age +_M.Cache_Control = http_caching.Cache_Control +_M.Expires = http_caching.Expires +_M.Pragma = http_caching.Pragma +_M.Warning = http_caching.Warning + +-- RFC 7235 +local http_authentication = require "lpeg_patterns.http.authentication" +_M.WWW_Authenticate = http_authentication.WWW_Authenticate +_M.Authorization = http_authentication.Authorization +_M.Proxy_Authenticate = http_authentication.Proxy_Authenticate +_M.Proxy_Authorization = http_authentication.Proxy_Authorization + +-- WebDav +local http_webdav = require "lpeg_patterns.http.webdav" +_M.CalDAV_Timezones = http_webdav.CalDAV_Timezones +_M.DASL = http_webdav.DASL +_M.DAV = http_webdav.DAV +_M.Depth = http_webdav.Depth +_M.Destination = http_webdav.Destination +_M.If = http_webdav.If +_M.If_Schedule_Tag_Match = http_webdav.If_Schedule_Tag_Match +_M.Lock_Token = http_webdav.Lock_Token +_M.Overwrite = http_webdav.Overwrite +_M.Schedule_Reply = http_webdav.Schedule_Reply +_M.Schedule_Tag = http_webdav.Schedule_Tag +_M.TimeOut = http_webdav.TimeOut + +-- RFC 5023 +_M.SLUG = require "lpeg_patterns.http.slug".SLUG + +-- RFC 5789 +_M.Accept_Patch = http_core.comma_sep_trim(http_semantics.media_type, 1) + +-- RFC 5988 +_M.Link = require "lpeg_patterns.http.link".Link + +-- RFC 6265 +local http_cookie = require "lpeg_patterns.http.cookie" +_M.Cookie = http_cookie.Cookie +_M.Set_Cookie = http_cookie.Set_Cookie + +-- RFC 6266 +_M.Content_Disposition = require "lpeg_patterns.http.disposition".Content_Disposition + +-- RFC 6454 +_M.Origin = require "lpeg_patterns.http.origin".Origin + +-- RFC 6455 +local http_websocket = require "lpeg_patterns.http.websocket" +_M.Sec_WebSocket_Accept = http_websocket.Sec_WebSocket_Accept +_M.Sec_WebSocket_Key = http_websocket.Sec_WebSocket_Key +_M.Sec_WebSocket_Extensions = http_websocket.Sec_WebSocket_Extensions +_M.Sec_WebSocket_Protocol_Client = http_websocket.Sec_WebSocket_Protocol_Client +_M.Sec_WebSocket_Protocol_Server = http_websocket.Sec_WebSocket_Protocol_Server +_M.Sec_WebSocket_Version_Client = http_websocket.Sec_WebSocket_Version_Client +_M.Sec_WebSocket_Version_Server = http_websocket.Sec_WebSocket_Version_Server + +-- RFC 6797 +_M.Strict_Transport_Security = require "lpeg_patterns.http.sts".Strict_Transport_Security + +-- RFC 7034 +_M.X_Frame_Options = require "lpeg_patterns.http.frameoptions".X_Frame_Options + +-- RFC 7089 +_M.Accept_Datetime = http_semantics.IMF_fixdate +_M.Memento_Datetime = http_semantics.IMF_fixdate + +-- RFC 7239 +_M.Forwarded = require "lpeg_patterns.http.forwarded".Forwarded + +-- RFC 7469 +local http_pkp = require "lpeg_patterns.http.pkp" +_M.Public_Key_Pins = http_pkp.Public_Key_Pins +_M.Public_Key_Pins_Report_Only = http_pkp.Public_Key_Pins_Report_Only + +-- RFC 7486 +_M.Hobareg = require "lpeg_patterns.http.hoba".Hobareg + +-- RFC 7615 +_M.Authentication_Info = http_authentication.Authentication_Info +_M.Proxy_Authentication_Info = http_authentication.Proxy_Authentication_Info + +-- RFC 7639 +_M.ALPN = require "lpeg_patterns.http.alpn".ALPN + +-- RFC 7838 +local http_alternate = require "lpeg_patterns.http.alternate" +_M.Alt_Svc = http_alternate.Alt_Svc +_M.Alt_Used = http_alternate.Alt_Used + +-- https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-06#section-2.1 +_M.Expect_CT = require "lpeg_patterns.http.expect_ct".Expect_CT + +-- https://www.w3.org/TR/referrer-policy/#referrer-policy-header +_M.Referrer_Policy = require "lpeg_patterns.http.referrer_policy".Referrer_Policy + +return _M diff --git a/framework/lualib/3rd/lpeg_patterns/http/alpn.lua b/framework/lualib/3rd/lpeg_patterns/http/alpn.lua new file mode 100755 index 0000000..d111fe4 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/alpn.lua @@ -0,0 +1,36 @@ +-- RFC 7639 + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" +local util = require "lpeg_patterns.util" + +local Cmt = lpeg.Cmt +local Cs = lpeg.Cs +local P = lpeg.P +local R = lpeg.R + +--[[ protocol-id is a percent-encoded ALPN protocol name + - Octets in the ALPN protocol MUST NOT be percent-encoded if they + are valid token characters except "%". + - When using percent-encoding, uppercase hex digits MUST be used. +]] + +local valid_chars = http_core.tchar - P"%" +local upper_hex = R("09", "AF") +local percent_char = P"%" * (upper_hex * upper_hex / util.read_hex) / string.char +local percent_encoded = Cmt(percent_char, function(_, _, c) + -- check that decoded character would not have been allowed unescaped + if not valid_chars:match(c) then + return true, c + end +end) +local percent_replace = Cs((valid_chars + percent_encoded)^0) + +local protocol_id = percent_replace + +local ALPN = http_core.comma_sep_trim(protocol_id, 1) + +return { + protocol_id = protocol_id; + ALPN = ALPN; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/alternate.lua b/framework/lualib/3rd/lpeg_patterns/http/alternate.lua new file mode 100755 index 0000000..7546e08 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/alternate.lua @@ -0,0 +1,23 @@ +-- RFC 7838 +-- HTTP Alternative Services + +local lpeg = require "lpeg" +local http_alpn = require "lpeg_patterns.http.alpn" +local http_core = require "lpeg_patterns.http.core" +local http_semantics = require "lpeg_patterns.http.semantics" +local uri = require "lpeg_patterns.uri" + +local C = lpeg.C +local P = lpeg.P + +local clear = C"clear" -- case-sensitive +local alt_authority = http_core.quoted_string -- containing [ uri_host ] ":" port +local alternative = http_alpn.protocol_id * P"=" * alt_authority +local alt_value = alternative * (http_core.OWS * P";" * http_core.OWS * http_semantics.parameter)^0 +local Alt_Svc = clear + http_core.comma_sep_trim(alt_value, 1) +local Alt_Used = uri.host * (P":" * uri.port)^-1 + +return { + Alt_Svc = Alt_Svc; + Alt_Used = Alt_Used; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/authentication.lua b/framework/lualib/3rd/lpeg_patterns/http/authentication.lua new file mode 100755 index 0000000..c8bffe6 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/authentication.lua @@ -0,0 +1,36 @@ +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" + +local C = lpeg.C +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local P = lpeg.P + +-- RFC 7235 Section 2 +local auth_scheme = http_core.token +local auth_param = Cg(http_core.token / string.lower * http_core.BWS * P"=" * http_core.BWS * (http_core.token + http_core.quoted_string)) +local token68 = C((core.ALPHA + core.DIGIT + P"-" + P"." + P"_" + P"~" + P"+" + P"/" )^1 * (P"=")^0) +-- TODO: each parameter name MUST only occur once per challenge +local challenge = auth_scheme * (core.SP^1 * (Cf(Ct(true) * http_core.comma_sep(auth_param), rawset) + token68))^-1 +local credentials = challenge + +-- RFC 7235 Section 4 +local WWW_Authenticate = http_core.comma_sep_trim(Ct(challenge), 1) +local Authorization = credentials +local Proxy_Authenticate = WWW_Authenticate +local Proxy_Authorization = Authorization + +-- RFC 7615 +local Authentication_Info = http_core.comma_sep_trim(auth_param) +local Proxy_Authentication_Info = http_core.comma_sep_trim(auth_param) + +return { + Authentication_Info = Authentication_Info; + Authorization = Authorization; + Proxy_Authenticate = Proxy_Authenticate; + Proxy_Authentication_Info = Proxy_Authentication_Info; + Proxy_Authorization = Proxy_Authorization; + WWW_Authenticate = WWW_Authenticate; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/caching.lua b/framework/lualib/3rd/lpeg_patterns/http/caching.lua new file mode 100755 index 0000000..fc3d020 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/caching.lua @@ -0,0 +1,46 @@ +-- RFC 7234 +-- Hypertext Transfer Protocol (HTTP/1.1): Caching + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local http_semantics = require "lpeg_patterns.http.semantics" +local uri = require "lpeg_patterns.uri" + +local Cc = lpeg.Cc +local Cg = lpeg.Cg +local P = lpeg.P + +-- RFC 7234 Section 1.2.1 +local delta_seconds = core.DIGIT^1 / tonumber + +-- RFC 7234 Section 5.1 +local Age = delta_seconds + +-- RFC 7234 Section 5.2 +local cache_directive = http_core.token / string.lower * ((P"=" * (http_core.token + http_core.quoted_string)) + Cc(true)) +local Cache_Control = http_core.comma_sep_trim(Cg(cache_directive), 1) + +-- RFC 7234 Section 5.3 +local Expires = http_semantics.HTTP_date + +-- RFC 7234 Section 5.4 +local extension_pragma = http_core.token * (P"=" * (http_core.token + http_core.quoted_string))^-1 +local pragma_directive = "no_cache" + extension_pragma +local Pragma = http_core.comma_sep_trim(pragma_directive, 1) + +-- RFC 7234 Section 5.5 +local warn_code = core.DIGIT * core.DIGIT * core.DIGIT +local warn_agent = (uri.host * (P":" * uri.port)^-1) + http_core.pseudonym +local warn_text = http_core.quoted_string +local warn_date = core.DQUOTE * http_semantics.HTTP_date * core.DQUOTE +local warning_value = warn_code * core.SP * warn_agent * core.SP * warn_text * (core.SP * warn_date)^-1 +local Warning = http_core.comma_sep_trim(warning_value, 1) + +return { + Age = Age; + Cache_Control = Cache_Control; + Expires = Expires; + Pragma = Pragma; + Warning = Warning; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/conditional.lua b/framework/lualib/3rd/lpeg_patterns/http/conditional.lua new file mode 100755 index 0000000..02bc430 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/conditional.lua @@ -0,0 +1,47 @@ +-- RFC 7232 +-- Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local http_semantics = require "lpeg_patterns.http.semantics" + +local C = lpeg.C +local Cc = lpeg.Cc +local Cg = lpeg.Cg +local P = lpeg.P +local R = lpeg.R + +-- RFC 7232 Section 2.2 +local Last_Modified = http_semantics.HTTP_date + +-- RFC 7232 Section 2.3 +local weak = P"W/" -- case sensitive +local etagc = P"\33" + R"\35\115" + http_core.obs_text +local opaque_tag = core.DQUOTE * etagc^0 * core.DQUOTE +local entity_tag = Cg(weak*Cc(true) + Cc(false), "weak") * C(opaque_tag) +local ETag = entity_tag + +-- RFC 7232 Section 3.1 +local If_Match = P"*" + http_core.comma_sep(entity_tag, 1) + +-- RFC 7232 Section 3.2 +local If_None_Match = P"*" + http_core.comma_sep(entity_tag, 1) + +-- RFC 7232 Section 3.3 +local If_Modified_Since = http_semantics.HTTP_date + +-- RFC 7232 Section 3.4 +local If_Unmodified_Since = http_semantics.HTTP_date + +return { + entity_tag = entity_tag; + opaque_tag = opaque_tag; + + Last_Modified = Last_Modified; + ETag = ETag; + If_Match = If_Match; + If_None_Match = If_None_Match; + If_Modified_Since = If_Modified_Since; + If_Unmodified_Since = If_Unmodified_Since; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/cookie.lua b/framework/lualib/3rd/lpeg_patterns/http/cookie.lua new file mode 100755 index 0000000..9056f5b --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/cookie.lua @@ -0,0 +1,37 @@ +-- RFC 6265 + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" + +local C = lpeg.C +local Cc = lpeg.Cc +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local P = lpeg.P +local R = lpeg.R +local S = lpeg.S + +local cookie_name = http_core.token +local cookie_octet = S"!" + R("\35\43", "\45\58", "\60\91", "\93\126") +local cookie_value = core.DQUOTE * C(cookie_octet^0) * core.DQUOTE + C(cookie_octet^0) +local cookie_pair = cookie_name * http_core.BWS * P"=" * http_core.BWS * cookie_value * http_core.BWS + +local ext_char = core.CHAR - core.CTL - S";" +ext_char = ext_char - core.WSP + core.WSP * #(core.WSP^0 * ext_char) -- No trailing whitespace +-- Complexity is to make sure whitespace before an `=` isn't captured +local extension_av = ((ext_char - S"=" - core.WSP) + core.WSP^1 * #(1-S"="))^0 / string.lower + * http_core.BWS * P"=" * http_core.BWS * C(ext_char^0) + + (ext_char)^0 / string.lower * Cc(true) +local cookie_av = extension_av +local set_cookie_string = cookie_pair * Cf(Ct(true) * (P";" * http_core.OWS * Cg(cookie_av))^0, rawset) +local Set_Cookie = set_cookie_string + +local cookie_string = Cf(Ct(true) * Cg(cookie_pair) * (P";" * http_core.OWS * Cg(cookie_pair))^0, rawset) +local Cookie = cookie_string + +return { + Cookie = Cookie; + Set_Cookie = Set_Cookie; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/core.lua b/framework/lualib/3rd/lpeg_patterns/http/core.lua new file mode 100755 index 0000000..f89cb64 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/core.lua @@ -0,0 +1,183 @@ +-- RFC 7230 +-- Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local uri = require "lpeg_patterns.uri" +local util = require "lpeg_patterns.util" + +local C = lpeg.C +local Cc = lpeg.Cc +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Cs = lpeg.Cs +local Ct = lpeg.Ct +local P = lpeg.P +local R = lpeg.R +local S = lpeg.S +local V = lpeg.V + +-- RFC 7230 Section 3.2.3 +local OWS = (core.SP + core.HTAB)^0 +local RWS = (core.SP + core.HTAB)^1 +local BWS = OWS + +-- Analogue to RFC 7230 Section 7's ABNF extension of '#' +-- Also documented as `#rule` under RFC 2616 Section 2.1 +local sep = OWS * lpeg.P "," * OWS +local optional_sep = (lpeg.P"," + core.SP + core.HTAB)^0 +local function comma_sep(element, min, max) + local extra = sep * optional_sep * element + local patt = element + if min then + for _=2, min do + patt = patt * extra + end + else + min = 0 + patt = patt^-1 + end + if max then + local more = max-min-1 + patt = patt * extra^-more + else + patt = patt * extra^0 + end + return patt +end +-- allows leading + trailing +local function comma_sep_trim(...) + return optional_sep * comma_sep(...) * optional_sep +end + +-- RFC 7230 Section 2.6 +local HTTP_name = P"HTTP" +local HTTP_version = HTTP_name * P"/" * (core.DIGIT * P"." * core.DIGIT / util.safe_tonumber) + +-- RFC 7230 Section 2.7 +local absolute_path = (P"/" * uri.segment )^1 +local partial_uri = Ct(uri.relative_part * (P"?" * uri.query)^-1) + +-- RFC 7230 Section 3.2.6 +local tchar = S "!#$%&'*+-.^_`|~" + core.DIGIT + core.ALPHA +local token = C(tchar^1) +local obs_text = R("\128\255") +local qdtext = core.HTAB + core.SP + P"\33" + R("\35\91", "\93\126") + obs_text +local quoted_pair = Cs(P"\\" * C(core.HTAB + core.SP + core.VCHAR + obs_text) / "%1") +local quoted_string = core.DQUOTE * Cs((qdtext + quoted_pair)^0) * core.DQUOTE + +local ctext = core.HTAB + core.SP + R("\33\39", "\42\91", "\93\126") + obs_text +local comment = P { P"(" * ( ctext + quoted_pair + V(1) )^0 * P")" } + +-- RFC 7230 Section 3.2 +local field_name = token / string.lower -- case insensitive +local field_vchar = core.VCHAR + obs_text +local field_content = field_vchar * (( core.SP + core.HTAB )^1 * field_vchar)^-1 +local obs_fold = ( core.SP + core.HTAB )^0 * core.CRLF * ( core.SP + core.HTAB )^1 / " " +-- field_value is not correct, see Errata: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 +local field_value = Cs((field_content + obs_fold)^0) +local header_field = field_name * P":" * OWS * field_value * OWS + +-- RFC 7230 Section 3.3.2 +local Content_Length = core.DIGIT^1 + +-- RFC 7230 Section 4 +-- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4683 +local transfer_parameter = (token - S"qQ" * BWS * P"=") * BWS * P"=" * BWS * ( token + quoted_string ) +local transfer_extension = Cf(Ct(token / string.lower) -- case insensitive + * ( OWS * P";" * OWS * Cg(transfer_parameter) )^0, rawset) +local transfer_coding = transfer_extension + +-- RFC 7230 Section 3.3.1 +local Transfer_Encoding = comma_sep_trim(transfer_coding, 1) + +-- RFC 7230 Section 4.1.1 +local chunk_ext_name = token +local chunk_ext_val = token + quoted_string +-- See https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4667 +local chunk_ext = ( P";" * chunk_ext_name * ( P"=" * chunk_ext_val)^-1 )^0 + +-- RFC 7230 Section 4.3 +local rank = (P"0" * ((P"." * core.DIGIT^-3) / util.safe_tonumber + Cc(0)) + P"1" * ("." * (P"0")^-3)^-1) * Cc(1) +local t_ranking = OWS * P";" * OWS * S"qQ" * P"=" * rank -- q is case insensitive +local t_codings = (transfer_coding * t_ranking^-1) / function(t, q) + if q then + t["q"] = q + end + return t +end +local TE = comma_sep_trim(t_codings) + +-- RFC 7230 Section 4.4 +local Trailer = comma_sep_trim(field_name, 1) + +-- RFC 7230 Section 5.3 +local origin_form = Cs(absolute_path * (P"?" * uri.query)^-1) +local absolute_form = util.no_rich_capture(uri.absolute_uri) +local authority_form = util.no_rich_capture(uri.authority) +local asterisk_form = C"*" +local request_target = asterisk_form + origin_form + absolute_form + authority_form + +-- RFC 7230 Section 3.1.1 +local method = token +local request_line = method * core.SP * request_target * core.SP * HTTP_version * core.CRLF + +-- RFC 7230 Section 5.4 +local Host = uri.host * (P":" * uri.port)^-1 + +-- RFC 7230 Section 6.7 +local protocol_name = token +local protocol_version = token +local protocol = protocol_name * (P"/" * protocol_version)^-1 / "%0" +local Upgrade = comma_sep_trim(protocol) + +-- RFC 7230 Section 5.7.1 +local received_protocol = (protocol_name * P"/" + Cc("HTTP")) * protocol_version / "%1/%2" +local pseudonym = token +-- workaround for https://lists.w3.org/Archives/Public/ietf-http-wg/2016OctDec/0527.html +local received_by = uri.host * ((P":" * uri.port) + -lpeg.B(",")) / "%0" + pseudonym +local Via = comma_sep_trim(Ct( + Cg(received_protocol, "protocol") + * RWS * Cg(received_by, "by") + * (RWS * Cg(comment, "comment"))^-1 +), 1) + +-- RFC 7230 Section 6.1 +local connection_option = token / string.lower -- case insensitive +local Connection = comma_sep_trim(connection_option) + +return { + comma_sep = comma_sep; + comma_sep_trim = comma_sep_trim; + + OWS = OWS; + RWS = RWS; + BWS = BWS; + + chunk_ext = chunk_ext; + comment = comment; + field_name = field_name; + field_value = field_value; + header_field = header_field; + method = method; + obs_text = obs_text; + partial_uri = partial_uri; + pseudonym = pseudonym; + qdtext = qdtext; + quoted_string = quoted_string; + rank = rank; + request_line = request_line; + request_target = request_target; + t_ranking = t_ranking; + tchar = tchar; + token = token; + + Connection = Connection; + Content_Length = Content_Length; + Host = Host; + TE = TE; + Trailer = Trailer; + Transfer_Encoding = Transfer_Encoding; + Upgrade = Upgrade; + Via = Via; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/disposition.lua b/framework/lualib/3rd/lpeg_patterns/http/disposition.lua new file mode 100755 index 0000000..fa26d4e --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/disposition.lua @@ -0,0 +1,27 @@ +-- RFC 6266 +-- Use of the Content-Disposition Header Field in the +-- Hypertext Transfer Protocol (HTTP) + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" +local http_parameters = require "lpeg_patterns.http.parameters" + +local C = lpeg.C +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local P = lpeg.P + +local disp_ext_type = http_core.token / string.lower +local disposition_type = disp_ext_type +-- can't use 'token' here as we need to not include the "*" at the end +local ext_token = C((http_core.tchar-P"*"*(-http_core.tchar))^1) * P"*" +local value = http_core.token + http_core.quoted_string +local disp_ext_parm = ext_token * http_core.OWS * P"=" * http_core.OWS * http_parameters.ext_value + + http_core.token * http_core.OWS * P"=" * http_core.OWS * value +local disposition_parm = disp_ext_parm +local Content_Disposition = disposition_type * Cf(Ct(true) * (http_core.OWS * P";" * http_core.OWS * Cg(disposition_parm))^0, rawset) + +return { + Content_Disposition = Content_Disposition; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/expect_ct.lua b/framework/lualib/3rd/lpeg_patterns/http/expect_ct.lua new file mode 100755 index 0000000..a19e927 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/expect_ct.lua @@ -0,0 +1,11 @@ +-- https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-06#section-2.1 + +local http_core = require "lpeg_patterns.http.core" +local http_utils = require "lpeg_patterns.http.util" + +local expect_ct_directive = http_utils.directive +local Expect_CT = http_utils.no_dup(http_core.comma_sep_trim(expect_ct_directive)) + +return { + Expect_CT = Expect_CT; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/forwarded.lua b/framework/lualib/3rd/lpeg_patterns/http/forwarded.lua new file mode 100755 index 0000000..603e24f --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/forwarded.lua @@ -0,0 +1,17 @@ +-- RFC 7239 +-- Forwarded HTTP Extension + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" + +local P = lpeg.P + +-- RFC 7239 Section 4 +local value = http_core.token + http_core.quoted_string +local forwarded_pair = http_core.token * P"=" * value +local forwarded_element = forwarded_pair^-1 * (P";" * forwarded_pair^-1)^0 +local Forwarded = http_core.comma_sep_trim(forwarded_element) + +return { + Forwarded = Forwarded; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/frameoptions.lua b/framework/lualib/3rd/lpeg_patterns/http/frameoptions.lua new file mode 100755 index 0000000..66eeb6d --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/frameoptions.lua @@ -0,0 +1,16 @@ +-- RFC 7034 + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" +local util = require "lpeg_patterns.util" + +local case_insensitive = util.case_insensitive +local Cc = lpeg.Cc + +local X_Frame_Options = case_insensitive "deny" * Cc("deny") + + case_insensitive "sameorigin" * Cc("sameorigin") + + case_insensitive "allow-from" * http_core.RWS * require "lpeg_patterns.http.origin".serialized_origin + +return { + X_Frame_Options = X_Frame_Options; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/hoba.lua b/framework/lualib/3rd/lpeg_patterns/http/hoba.lua new file mode 100755 index 0000000..d9755ad --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/hoba.lua @@ -0,0 +1,12 @@ +-- RFC 7486 +-- HTTP Origin-Bound Authentication (HOBA) + +local lpeg = require "lpeg" + +local C = lpeg.C + +local Hobareg = C"regok" + C"reginwork" + +return { + Hobareg = Hobareg; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/link.lua b/framework/lualib/3rd/lpeg_patterns/http/link.lua new file mode 100755 index 0000000..e1c7953 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/link.lua @@ -0,0 +1,30 @@ +-- RFC 5988 + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local http_parameters = require "lpeg_patterns.http.parameters" +local uri = require "lpeg_patterns.uri" + +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local P = lpeg.P +local S = lpeg.S + +local ptokenchar = S"!#$%&'()*+-./:<=>?@[]^_`{|}~" + core.DIGIT + core.ALPHA +local ptoken = ptokenchar^1 +local ext_name_star = http_parameters.parmname * P"*" +local link_extension = ext_name_star * P"=" * http_parameters.ext_value + + http_parameters.parmname * (P"=" * (ptoken + http_core.quoted_string))^-1 +-- See https://www.rfc-editor.org/errata_search.php?rfc=5988&eid=3158 +local link_param = link_extension +local link_value = Cf(Ct(P"<" * uri.uri_reference * P">") * (http_core.OWS * P";" * http_core.OWS * Cg(link_param))^0, rawset) +-- TODO: handle multiple ext_value variants... +-- e.g. server might provide one title in english, one in chinese, client should be able to pick which one to display + +local Link = http_core.comma_sep_trim(link_value) + +return { + Link = Link; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/origin.lua b/framework/lualib/3rd/lpeg_patterns/http/origin.lua new file mode 100755 index 0000000..09da2f0 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/origin.lua @@ -0,0 +1,20 @@ +-- RFC 6454 + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local uri = require "lpeg_patterns.uri" + +local C = lpeg.C +local P = lpeg.P + +-- discard captures from scheme, host, port and just get whole string +local serialized_origin = C(uri.scheme * P"://" * uri.host * (P":" * uri.port)^-1/function() end) +local origin_list = serialized_origin * (core.SP * serialized_origin)^0 +local origin_list_or_null = P"null" + origin_list +local Origin = http_core.OWS * origin_list_or_null * http_core.OWS + +return { + serialized_origin = serialized_origin; + Origin = Origin; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/parameters.lua b/framework/lualib/3rd/lpeg_patterns/http/parameters.lua new file mode 100755 index 0000000..f067940 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/parameters.lua @@ -0,0 +1,29 @@ +-- RFC 5987 +-- Character Set and Language Encoding for +-- Hypertext Transfer Protocol (HTTP) Header Field Parameters + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local language = require "lpeg_patterns.language" +local util = require "lpeg_patterns.util" + +local C = lpeg.C +local Cg = lpeg.Cg +local Cs = lpeg.Cs +local P = lpeg.P +local S = lpeg.S + +local attr_char = core.ALPHA + core.DIGIT + S"!#$&+-.^_`|~" +-- can't use uri.pct_encoded, as it doesn't decode all characters +local pct_encoded = P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char +local value_chars = Cs((pct_encoded + attr_char)^0) +local parmname = C(attr_char^1) +-- ext-value uses charset from RFC 5987 +local mime_charsetc = core.ALPHA + core.DIGIT + S"!#$%&+-^_`{}~" +local mime_charset = C(mime_charsetc^1) +local ext_value = Cg(mime_charset, "charset") * P"'" * Cg(language.Language_Tag, "language")^-1 * P"'" * value_chars + +return { + ext_value = ext_value; + parmname = parmname; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/pkp.lua b/framework/lualib/3rd/lpeg_patterns/http/pkp.lua new file mode 100755 index 0000000..e3c7393 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/pkp.lua @@ -0,0 +1,54 @@ +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" +local http_utils = require "lpeg_patterns.http.util" + +local Cmt = lpeg.Cmt +local P = lpeg.P + +-- RFC 7469 +local Public_Key_Directives = http_utils.directive * (http_core.OWS * P";" * http_core.OWS * http_utils.directive)^0 +local function pkp_cmt(pins, t, k, v, ...) + -- duplicates are allowed if the directive name starts with "pin-" + local pin_name = k:match("^pin%-(.+)") + if pin_name then + local hashes = pins[pin_name] + if hashes then + hashes[#hashes+1] = v + else + hashes = {v} + pins[pin_name] = hashes + end + else + local old = t[k] + if old then + return false + end + t[k] = v + end + if ... then + return pkp_cmt(pins, t, ...) + else + return true + end +end +local Public_Key_Pins = Cmt(Public_Key_Directives, function(_, _, ...) + local pins = {} + local t = {} + local ok = pkp_cmt(pins, t, ...) + if ok and t["max-age"] then + return true, pins, t + end +end) +local Public_Key_Pins_Report_Only = Cmt(Public_Key_Directives, function(_, _, ...) + local pins = {} + local t = {} + local ok = pkp_cmt(pins, t, ...) + if ok then + return true, pins, t + end +end) + +return { + Public_Key_Pins = Public_Key_Pins; + Public_Key_Pins_Report_Only = Public_Key_Pins_Report_Only; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/range.lua b/framework/lualib/3rd/lpeg_patterns/http/range.lua new file mode 100755 index 0000000..d979881 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/range.lua @@ -0,0 +1,53 @@ +-- RFC 7233 +-- Hypertext Transfer Protocol (HTTP/1.1): Range Requests + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_conditional = require "lpeg_patterns.http.conditional" +local http_core = require "lpeg_patterns.http.core" +local http_semantics = require "lpeg_patterns.http.semantics" + +local C = lpeg.C +local Cc = lpeg.Cc +local P = lpeg.P + +local bytes_unit = P"bytes" +local other_range_unit = http_core.token +local range_unit = C(bytes_unit) + other_range_unit + +local first_byte_pos = core.DIGIT^1 / tonumber +local last_byte_pos = core.DIGIT^1 / tonumber +local byte_range_spec = first_byte_pos * P"-" * last_byte_pos^-1 +local suffix_length = core.DIGIT^1 / tonumber +local suffix_byte_range_spec = Cc(nil) * P"-" * suffix_length +local byte_range_set = http_core.comma_sep(byte_range_spec + suffix_byte_range_spec, 1) +local byte_ranges_specifier = bytes_unit * P"=" * byte_range_set + +-- RFC 7233 Section 2.3 +local acceptable_ranges = http_core.comma_sep_trim(range_unit, 1) + P"none" +local Accept_Ranges = acceptable_ranges + +-- RFC 7233 Section 3.1 +local other_range_set = core.VCHAR^1 +local other_ranges_specifier = other_range_unit * P"=" * other_range_set +local Range = byte_ranges_specifier + other_ranges_specifier + +-- RFC 7233 Section 3.2 +local If_Range = http_conditional.entity_tag + http_semantics.HTTP_date + +-- RFC 7233 Section 4.2 +local complete_length = core.DIGIT^1 / tonumber +local unsatisfied_range = P"*/" * complete_length +local byte_range = first_byte_pos * P"-" * last_byte_pos +local byte_range_resp = byte_range * P"/" * (complete_length + P"*") +local byte_content_range = bytes_unit * core.SP * (byte_range_resp + unsatisfied_range) +local other_range_resp = core.CHAR^0 +local other_content_range = other_range_unit * core.SP * other_range_resp +local Content_Range = byte_content_range + other_content_range + +return { + Accept_Ranges = Accept_Ranges; + Range = Range; + If_Range = If_Range; + Content_Range = Content_Range; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/referrer_policy.lua b/framework/lualib/3rd/lpeg_patterns/http/referrer_policy.lua new file mode 100755 index 0000000..0b2be18 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/referrer_policy.lua @@ -0,0 +1,20 @@ +-- https://www.w3.org/TR/referrer-policy/#referrer-policy-header + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" + +local C = lpeg.C + +local policy_token = C"no-referrer" + + C"no-referrer-when-downgrade" + + C"strict-origin" + + C"strict-origin-when-cross-origin" + + C"same-origin" + + C"origin" + + C"origin-when-cross-origin" + + C"unsafe-url" +local Referrer_Policy = http_core.comma_sep_trim(policy_token, 1) + +return { + Referrer_Policy = Referrer_Policy; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/semantics.lua b/framework/lualib/3rd/lpeg_patterns/http/semantics.lua new file mode 100755 index 0000000..015fc6a --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/semantics.lua @@ -0,0 +1,186 @@ +-- RFC 7231 +-- Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local email = require "lpeg_patterns.email" +local language = require "lpeg_patterns.language" +local uri = require "lpeg_patterns.uri" + +local Cc = lpeg.Cc +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local P = lpeg.P +local S = lpeg.S + + +-- RFC 7231 Section 3.1.1 +local content_coding = http_core.token / string.lower -- case insensitive +local Content_Encoding = http_core.comma_sep_trim(content_coding, 1) + +-- RFC 7231 Section 3.1.2 +local type = http_core.token / string.lower -- case insensitive +local subtype = http_core.token / string.lower -- case insensitive +local parameter = http_core.token / string.lower -- case insensitive + * P"=" * (http_core.token + http_core.quoted_string) +local media_type = Cg(type, "type") * P"/" * Cg(subtype, "subtype") + * Cg(Cf(Ct(true) * (http_core.OWS * P";" * http_core.OWS * Cg(parameter))^0, rawset), "parameters") +local charset = http_core.token / string.lower -- case insensitive +local Content_Type = Ct(media_type) + +-- RFC 7231 Section 3.1.3 +local Content_Language = http_core.comma_sep_trim(language.Language_Tag, 1) + +-- RFC 7231 Section 3.1.4.2 +local Content_Location = uri.absolute_uri + http_core.partial_uri + +-- RFC 7231 Section 5.1.1 +local Expect = P"100-"*S"cC"*S"oO"*S"nN"*S"tT"*S"iI"*S"nN"*S"uU"*S"eE" * Cc("100-continue") + +-- RFC 7231 Section 5.1.2 +local Max_Forwards = core.DIGIT^1 / tonumber + +-- RFC 7231 Section 5.3.1 +-- local qvalue = http_core.rank -- luacheck: ignore 211 +local weight = http_core.t_ranking + +-- RFC 7231 Section 5.3.2 +local media_range = (P"*/*" + + (Cg(type, "type") * P"/*") + + (Cg(type, "type") * P"/" * Cg(subtype, "subtype")) +) * Cg(Cf(Ct(true) * (http_core.OWS * ";" * http_core.OWS * Cg(parameter) - weight)^0, rawset), "parameters") +local accept_ext = http_core.OWS * P";" * http_core.OWS * http_core.token * (P"=" * (http_core.token + http_core.quoted_string))^-1 +local accept_params = Cg(weight, "q") * Cg(Cf(Ct(true) * Cg(accept_ext)^0, rawset), "extensions") +local Accept = http_core.comma_sep_trim(Ct(media_range * (accept_params+Cg(Ct(true), "extensions")))) + +-- RFC 7231 Section 5.3.3 +local Accept_Charset = http_core.comma_sep_trim((charset + P"*") * weight^-1, 1) + +-- RFC 7231 Section 5.3.4 +local codings = content_coding + "*" +local Accept_Encoding = http_core.comma_sep_trim(codings * weight^-1) + +-- RFC 4647 Section 2.1 +local alphanum = core.ALPHA + core.DIGIT +local language_range = (core.ALPHA * core.ALPHA^-7 * (P"-" * alphanum * alphanum^-7)^0) + P"*" +-- RFC 7231 Section 5.3.5 +local Accept_Language = http_core.comma_sep_trim(language_range * weight^-1, 1) + +-- RFC 7231 Section 5.5.1 +local From = email.mailbox + +-- RFC 7231 Section 5.5.2 +local Referer = uri.absolute_uri + http_core.partial_uri + +-- RFC 7231 Section 5.5.3 +local product_version = http_core.token +local product = http_core.token * (P"/" * product_version)^-1 +local User_Agent = product * (http_core.RWS * (product + http_core.comment))^0 + +-- RFC 7231 Section 7.1.1.1 +-- Uses os.date field names +local day_name = Cg(P"Mon"*Cc(2) + + P"Tue"*Cc(3) + + P"Wed"*Cc(4) + + P"Thu"*Cc(5) + + P"Fri"*Cc(6) + + P"Sat"*Cc(7) + + P"Sun"*Cc(1), "wday") +local day = Cg(core.DIGIT * core.DIGIT / tonumber, "day") +local month = Cg(P"Jan"*Cc(1) + + P"Feb"*Cc(2) + + P"Mar"*Cc(3) + + P"Apr"*Cc(4) + + P"May"*Cc(5) + + P"Jun"*Cc(6) + + P"Jul"*Cc(7) + + P"Aug"*Cc(8) + + P"Sep"*Cc(9) + + P"Oct"*Cc(10) + + P"Nov"*Cc(11) + + P"Dec"*Cc(12), "month") +local year = Cg(core.DIGIT * core.DIGIT * core.DIGIT * core.DIGIT / tonumber, "year") +local date1 = day * core.SP * month * core.SP * year + +local GMT = P"GMT" + +local minute = Cg(core.DIGIT * core.DIGIT / tonumber, "min") +local second = Cg(core.DIGIT * core.DIGIT / tonumber, "sec") +local hour = Cg(core.DIGIT * core.DIGIT / tonumber, "hour") +-- XXX only match 00:00:00 - 23:59:60 (leap second)? + +local time_of_day = hour * P":" * minute * P":" * second +local IMF_fixdate = Ct(day_name * P"," * core.SP * date1 * core.SP * time_of_day * core.SP * GMT) + +local date2 do + local year_barrier = 70 + local twodayyear = Cg(core.DIGIT * core.DIGIT / function(y) + y = tonumber(y, 10) + if y < year_barrier then + return 2000+y + else + return 1900+y + end + end, "year") + date2 = day * P"-" * month * P"-" * twodayyear +end +local day_name_l = Cg(P"Monday"*Cc(2) + + P"Tuesday"*Cc(3) + + P"Wednesday"*Cc(4) + + P"Thursday"*Cc(5) + + P"Friday"*Cc(6) + + P"Saturday"*Cc(7) + + P"Sunday"*Cc(1), "wday") +local rfc850_date = Ct(day_name_l * P"," * core.SP * date2 * core.SP * time_of_day * core.SP * GMT) + +local date3 = month * core.SP * (day + Cg(core.SP * core.DIGIT / tonumber, "day")) +local asctime_date = Ct(day_name * core.SP * date3 * core.SP * time_of_day * core.SP * year) +local obs_date = rfc850_date + asctime_date + +local HTTP_date = IMF_fixdate + obs_date +local Date = HTTP_date + +-- RFC 7231 Section 7.1.2 +local Location = uri.uri_reference + +-- RFC 7231 Section 7.1.3 +local delay_seconds = core.DIGIT^1 / tonumber +local Retry_After = HTTP_date + delay_seconds + +-- RFC 7231 Section 7.1.4 +local Vary = P"*" + http_core.comma_sep(http_core.field_name, 1) + +-- RFC 7231 Section 7.4.1 +local Allow = http_core.comma_sep_trim(http_core.method) + +-- RFC 7231 Section 7.4.2 +local Server = product * (http_core.RWS * (product + http_core.comment))^0 + +return { + HTTP_date = HTTP_date; + IMF_fixdate = IMF_fixdate; + media_type = media_type; + parameter = parameter; + + Accept = Accept; + Accept_Charset = Accept_Charset; + Accept_Encoding = Accept_Encoding; + Accept_Language = Accept_Language; + Allow = Allow; + Content_Encoding = Content_Encoding; + Content_Language = Content_Language; + Content_Location = Content_Location; + Content_Type = Content_Type; + Date = Date; + Expect = Expect; + From = From; + Location = Location; + Max_Forwards = Max_Forwards; + Referer = Referer; + Retry_After = Retry_After; + Server = Server; + User_Agent = User_Agent; + Vary = Vary; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/slug.lua b/framework/lualib/3rd/lpeg_patterns/http/slug.lua new file mode 100755 index 0000000..06cf9af --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/slug.lua @@ -0,0 +1,20 @@ +-- RFC 5023 + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" +local util = require "lpeg_patterns.util" + +local Cs = lpeg.Cs +local P = lpeg.P +local R = lpeg.R + +local slugtext = http_core.RWS / " " + + P"%" * (core.HEXDIG * core.HEXDIG / util.read_hex) / string.char + + R"\32\126" + +local SLUG = Cs(slugtext^0) + +return { + SLUG = SLUG; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/sts.lua b/framework/lualib/3rd/lpeg_patterns/http/sts.lua new file mode 100755 index 0000000..3732b68 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/sts.lua @@ -0,0 +1,13 @@ +-- RFC 6797 + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" +local http_utils = require "lpeg_patterns.http.util" + +local P = lpeg.P + +local Strict_Transport_Security = http_utils.no_dup(http_utils.directive^-1 * (http_core.OWS * P";" * http_core.OWS * http_utils.directive^-1)^0) + +return { + Strict_Transport_Security = Strict_Transport_Security; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/util.lua b/framework/lualib/3rd/lpeg_patterns/http/util.lua new file mode 100755 index 0000000..0cf0ec8 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/util.lua @@ -0,0 +1,38 @@ +-- This is a private module containing utility functions shared by various http parsers + +local lpeg = require "lpeg" +local http_core = require "lpeg_patterns.http.core" + +local P = lpeg.P +local Cc = lpeg.Cc +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local Cmt = lpeg.Cmt + +local directive_name = http_core.token / string.lower +local directive_value = http_core.token + http_core.quoted_string +local directive = Cg(directive_name * ((http_core.OWS * P"=" * http_core.OWS * directive_value) + Cc(true))) + +-- Helper function that doesn't match if there are duplicate keys +local function no_dup_cmt(s, i, t, name, value, ...) + local old = t[name] + if old then + return false + end + t[name] = value + if ... then + return no_dup_cmt(s, i, t, ...) + elseif t["max-age"] then -- max-age is required + return true, t + end + -- else return nil +end + +local function no_dup(patt) + return Cmt(Ct(true) * patt, no_dup_cmt) +end + +return { + directive = directive; + no_dup = no_dup; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/webdav.lua b/framework/lualib/3rd/lpeg_patterns/http/webdav.lua new file mode 100755 index 0000000..3e99c07 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/webdav.lua @@ -0,0 +1,67 @@ +-- WebDAV + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_conditional = require "lpeg_patterns.http.conditional" +local http_core = require "lpeg_patterns.http.core" +local uri = require "lpeg_patterns.uri" +local util = require "lpeg_patterns.util" + +local case_insensitive = util.case_insensitive + +local Cc = lpeg.Cc +local P = lpeg.P +local S = lpeg.S + +local T_F = S"Tt" * Cc(true) + S"Ff" * Cc(false) + +-- RFC 4918 +local Coded_URL = P"<" * uri.absolute_uri * P">" +local extend = Coded_URL + http_core.token +local compliance_class = P"1" + P"2" + P"3" + extend +local DAV = http_core.comma_sep_trim(compliance_class) +local Depth = P"0" * Cc(0) + + P"1" * Cc(1) + + case_insensitive "infinity" * Cc(math.huge) +local Simple_ref = uri.absolute_uri + http_core.partial_uri +local Destination = Simple_ref +local State_token = Coded_URL +local Condition = (case_insensitive("not") * Cc("not"))^-1 + * http_core.OWS * (State_token + P"[" * http_conditional.entity_tag * P"]") +local List = P"(" * http_core.OWS * (Condition * http_core.OWS)^1 * P")" +local No_tag_list = List +local Resource_Tag = P"<" * Simple_ref * P">" +local Tagged_list = Resource_Tag * http_core.OWS * (List * http_core.OWS)^1 +local If = (Tagged_list * http_core.OWS)^1 + (No_tag_list * http_core.OWS)^1 +local Lock_Token = Coded_URL +local Overwrite = T_F +local DAVTimeOutVal = core.DIGIT^1 / tonumber +local TimeType = case_insensitive "Second-" * DAVTimeOutVal + + case_insensitive "Infinite" * Cc(math.huge) +local TimeOut = http_core.comma_sep_trim(TimeType) + +-- RFC 5323 +local DASL = http_core.comma_sep_trim(Coded_URL, 1) + +-- RFC 6638 +local Schedule_Reply = T_F +local Schedule_Tag = http_conditional.opaque_tag +local If_Schedule_Tag_Match = http_conditional.opaque_tag + +-- RFC 7809 +local CalDAV_Timezones = T_F + +return { + CalDAV_Timezones = CalDAV_Timezones; + DASL = DASL; + DAV = DAV; + Depth = Depth; + Destination = Destination; + If = If; + If_Schedule_Tag_Match = If_Schedule_Tag_Match; + Lock_Token = Lock_Token; + Overwrite = Overwrite; + Schedule_Reply = Schedule_Reply; + Schedule_Tag = Schedule_Tag; + TimeOut = TimeOut; +} diff --git a/framework/lualib/3rd/lpeg_patterns/http/websocket.lua b/framework/lualib/3rd/lpeg_patterns/http/websocket.lua new file mode 100755 index 0000000..51e723e --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/http/websocket.lua @@ -0,0 +1,55 @@ +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" +local http_core = require "lpeg_patterns.http.core" + +local Cc = lpeg.Cc +local Cf = lpeg.Cf +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local Cmt = lpeg.Cmt +local P = lpeg.P +local S = lpeg.S + +-- RFC 6455 +local base64_character = core.ALPHA + core.DIGIT + S"+/" +local base64_data = base64_character * base64_character * base64_character * base64_character +local base64_padding = base64_character * base64_character * P"==" + + base64_character * base64_character * base64_character * P"=" +local base64_value_non_empty = (base64_data^1 * base64_padding^-1) + base64_padding +local Sec_WebSocket_Accept = base64_value_non_empty +local Sec_WebSocket_Key = base64_value_non_empty +local registered_token = http_core.token +local extension_token = registered_token +local extension_param do + local EOF = P(-1) + local token_then_EOF = Cc(true) * http_core.token * EOF + -- the quoted-string must be a valid token + local quoted_token = Cmt(http_core.quoted_string, function(_, _, q) + return token_then_EOF:match(q) + end) + extension_param = http_core.token * ((P"=" * (http_core.token + quoted_token)) + Cc(true)) +end +local extension = extension_token * Cg(Cf(Ct(true) * (P";" * Cg(extension_param))^0, rawset), "parameters") +local extension_list = http_core.comma_sep_trim(Ct(extension)) +local Sec_WebSocket_Extensions = extension_list +local Sec_WebSocket_Protocol_Client = http_core.comma_sep_trim(http_core.token) +local Sec_WebSocket_Protocol_Server = http_core.token +local NZDIGIT = S"123456789" +-- Limited to 0-255 range, with no leading zeros +local version = ( + P"2" * (S"01234" * core.DIGIT + P"5" * S"012345") + + (P"1") * core.DIGIT * core.DIGIT + + NZDIGIT * core.DIGIT^-1 +) / tonumber +local Sec_WebSocket_Version_Client = version +local Sec_WebSocket_Version_Server = http_core.comma_sep_trim(version) + +return { + Sec_WebSocket_Accept = Sec_WebSocket_Accept; + Sec_WebSocket_Key = Sec_WebSocket_Key; + Sec_WebSocket_Extensions = Sec_WebSocket_Extensions; + Sec_WebSocket_Protocol_Client = Sec_WebSocket_Protocol_Client; + Sec_WebSocket_Protocol_Server = Sec_WebSocket_Protocol_Server; + Sec_WebSocket_Version_Client = Sec_WebSocket_Version_Client; + Sec_WebSocket_Version_Server = Sec_WebSocket_Version_Server; +} diff --git a/framework/lualib/3rd/lpeg_patterns/language.lua b/framework/lualib/3rd/lpeg_patterns/language.lua new file mode 100755 index 0000000..9bafd77 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/language.lua @@ -0,0 +1,82 @@ +-- RFC 5646 Section 2.1 + +local lpeg = require "lpeg" +local core = require "lpeg_patterns.core" + +local C = lpeg.C +local P = lpeg.P +local R = lpeg.R +local Cg = lpeg.Cg +local Ct = lpeg.Ct +local Cmt = lpeg.Cmt + +local M = {} + +local alphanum = core.ALPHA + core.DIGIT + +local extlang = core.ALPHA * core.ALPHA * core.ALPHA * -#alphanum + * (P"-" * core.ALPHA * core.ALPHA * core.ALPHA * -#alphanum)^-2 + +local language = Cg(core.ALPHA * core.ALPHA * core.ALPHA * core.ALPHA * core.ALPHA * core.ALPHA^-3, "language") + + Cg(core.ALPHA * core.ALPHA * core.ALPHA * core.ALPHA, "language") + + Cg(core.ALPHA * core.ALPHA * core.ALPHA^-1, "language") * (P"-" * Cg(extlang, "extlang"))^-1 + +local script = core.ALPHA * core.ALPHA * core.ALPHA * core.ALPHA + * -#alphanum -- Prevent intepretation of a 'variant' + +local region = ( + core.ALPHA * core.ALPHA + + core.DIGIT * core.DIGIT * core.DIGIT +) * -#alphanum -- Prevent intepretation of a 'variant' + +local variant = core.DIGIT * alphanum * alphanum * alphanum + + alphanum * alphanum * alphanum * alphanum * alphanum * alphanum^-3 + +local singleton = core.DIGIT + R("AW", "YZ", "aw", "yz") + +local extension = C(singleton) * Ct((P"-" * (alphanum*alphanum*alphanum^-6 / string.lower))^1) + +M.privateuse = P"x" * Ct((P"-" * C(alphanum*alphanum^-7))^1) + +M.langtag = language + * (P"-" * Cg(script, "script"))^-1 + * (P"-" * Cg(region, "region"))^-1 + * Cg(Ct((P"-" * C(variant))^1), "variant")^-1 + * Cg(Cmt(Ct((P"-" * Ct(extension))^1), function(_, _, c) + -- Can't use a fold with rawset as we want the pattern to not match if there is a duplicate extension + local r = {} + for _, v in ipairs(c) do + local a, b = v[1], v[2] + if r[a] then + -- duplicate extension + return false + end + r[a] = b + end + return true, r + end), "extension")^-1 + * (P"-" * Cg(M.privateuse, "privateuse"))^-1 + +local irregular = P"en-GB-oed" + + P"i-ami" + + P"i-bnn" + + P"i-default" + + P"i-enochian" + + P"i-hak" + + P"i-klingon" + + P"i-lux" + + P"i-mingo" + + P"i-navajo" + + P"i-pwn" + + P"i-tao" + + P"i-tay" + + P"i-tsu" + + P"sgn-BE-FR" + + P"sgn-BE-NL" + + P"sgn-CH-DE" + +M.Language_Tag = C((M.langtag + + M.privateuse + + irregular) / function() end) -- capture the whole tag. throws away decomposition + +return M diff --git a/framework/lualib/3rd/lpeg_patterns/phone.lua b/framework/lualib/3rd/lpeg_patterns/phone.lua new file mode 100755 index 0000000..22836d0 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/phone.lua @@ -0,0 +1,77 @@ +-- Phone numbers + +local lpeg = require "lpeg" +local P = lpeg.P +local R = lpeg.R +local S = lpeg.S + +local digit = R"09" +local seperator = S"- ,." +local function optional_parens(patt) + return P"(" * patt * P")" + patt +end + +local _M = {} + +local extension = P"e" * (P"xt")^-1 * seperator^-1 * digit^1 +local optional_extension = (seperator^-1 * extension)^-1 + +_M.Australia = ( + -- Normal landlines + optional_parens((P"0")^-1*S"2378") * seperator^-1 * digit*digit*digit*digit * seperator^-1 * digit*digit*digit*digit + -- Mobile numbers + + (optional_parens(P"0"*S"45"*digit*digit) + S"45"*digit*digit) + * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit + -- Local rate calls + + P"1300" * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit + -- 1345 is only used for back-to-base monitored alarm systems + + P"1345" * seperator^-1 * digit*digit * seperator^-1 * digit*digit + + P"13" * seperator^-1 * digit*digit * seperator^-1 * digit*digit + + (P"0")^-1*P"198" * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit -- data calls + -- Free calls + + P"1800" * seperator^-1 * digit*digit*digit * seperator^-1 * digit*digit*digit + + P"180" * seperator^-1 * digit*digit*digit*digit +) * optional_extension + +local NPA = (digit-S"01")*digit*digit +local NXX = ((digit-S"01")*(digit-P"9")-P"37"-P"96")*digit-P(1)*P"11" +local USSubscriber = digit*digit*digit*digit +_M.USA = ((P"1" * seperator^-1)^-1 * optional_parens(NPA) * seperator^-1)^-1 + * NXX * seperator^-1 * USSubscriber * optional_extension + +local international = ( + P"1" * seperator^-1 * #(-P"1") * _M.USA + + P"61" * seperator^-1 * #(digit-P"0") * _M.Australia + -- Other countries we haven't made specific patterns for yet + +(P"20"+P"212"+P"213"+P"216"+P"218"+P"220"+P"221" + +P"222"+P"223"+P"224"+P"225"+P"226"+P"227"+P"228"+P"229" + +P"230"+P"231"+P"232"+P"233"+P"234"+P"235"+P"236"+P"237" + +P"238"+P"239"+P"240"+P"241"+P"242"+P"243"+P"244"+P"245" + +P"246"+P"247"+P"248"+P"249"+P"250"+P"251"+P"252"+P"253" + +P"254"+P"255"+P"256"+P"257"+P"258"+P"260"+P"261"+P"262" + +P"263"+P"264"+P"265"+P"266"+P"267"+P"268"+P"269"+P"27" + +P"290"+P"291"+P"297"+P"298"+P"299"+P"30" +P"31" +P"32" + +P"33" +P"34" +P"350"+P"351"+P"352"+P"353"+P"354"+P"355" + +P"356"+P"357"+P"358"+P"359"+P"36" +P"370"+P"371"+P"372" + +P"373"+P"374"+P"375"+P"376"+P"377"+P"378"+P"380"+P"381" + +P"385"+P"386"+P"387"+P"389"+P"39" +P"40" +P"41" +P"420" + +P"421"+P"423"+P"43" +P"44" +P"45" +P"46" +P"47" +P"48" + +P"49" +P"500"+P"501"+P"502"+P"503"+P"504"+P"505"+P"506" + +P"507"+P"508"+P"509"+P"51" +P"52" +P"53" +P"54" +P"55" + +P"56" +P"57" +P"58" +P"590"+P"591"+P"592"+P"593"+P"594" + +P"595"+P"596"+P"597"+P"598"+P"599"+P"60" +P"62" + +P"63" +P"64" +P"65" +P"66" +P"670"+P"672"+P"673"+P"674" + +P"675"+P"676"+P"677"+P"678"+P"679"+P"680"+P"681"+P"682" + +P"683"+P"684"+P"685"+P"686"+P"687"+P"688"+P"689"+P"690" + +P"691"+P"692"+P"7" +P"808"+P"81" +P"82" +P"84" +P"850" + +P"852"+P"853"+P"855"+P"856"+P"86" +P"870"+P"871"+P"872" + +P"873"+P"874"+P"878"+P"880"+P"881"+P"886"+P"90" +P"91" + +P"92" +P"93" +P"94" +P"95" +P"960"+P"961"+P"962"+P"963" + +P"964"+P"965"+P"966"+P"967"+P"968"+P"970"+P"971"+P"972" + +P"973"+P"974"+P"975"+P"976"+P"977"+P"98" +P"992"+P"993" + +P"994"+P"995"+P"996"+P"998" ) * (seperator^-1*digit)^6 -- At least 6 digits +) + +_M.phone = P"+" * seperator^-1 * international + +return _M diff --git a/framework/lualib/3rd/lpeg_patterns/uri.lua b/framework/lualib/3rd/lpeg_patterns/uri.lua new file mode 100755 index 0000000..1b0dfda --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/uri.lua @@ -0,0 +1,143 @@ +-- URI +-- RFC 3986 + +local lpeg = require "lpeg" +local P = lpeg.P +local S = lpeg.S +local C = lpeg.C +local Cc = lpeg.Cc +local Cg = lpeg.Cg +local Cs = lpeg.Cs +local Ct = lpeg.Ct + +local util = require "lpeg_patterns.util" + +local core = require "lpeg_patterns.core" +local ALPHA = core.ALPHA +local DIGIT = core.DIGIT +local HEXDIG = core.HEXDIG + +local IPv4address = require "lpeg_patterns.IPv4".IPv4address +local IPv6address = require "lpeg_patterns.IPv6".IPv6address + +local _M = {} + +_M.sub_delims = S"!$&'()*+,;=" -- 2.2 +local unreserved = ALPHA + DIGIT + S"-._~" -- 2.3 +_M.pct_encoded = P"%" * (HEXDIG * HEXDIG / util.read_hex) / function(n) + local c = string.char(n) + if unreserved:match(c) then + -- always decode unreserved characters (2.3) + return c + else + -- normalise to upper-case (6.2.2.1) + return string.format("%%%02X", n) + end +end -- 2.1 + +_M.scheme = ALPHA * (ALPHA + DIGIT + S"+-.")^0 / string.lower -- 3.1 + +_M.userinfo = Cs((unreserved + _M.pct_encoded + _M.sub_delims + P":")^0) -- 3.2.1 + +-- Host 3.2.2 + +local IPvFuture_mt = { + __name = "lpeg_patterns.IPvFuture"; +} +function IPvFuture_mt:__tostring() + return string.format("v%x.%s", self.version, self.string) +end +local function new_IPvFuture(version, string) + return setmetatable({version=version, string=string}, IPvFuture_mt) +end +local IPvFuture = S"vV" * (HEXDIG^1/util.read_hex) * P"." * C((unreserved+_M.sub_delims+P":")^1) / new_IPvFuture + +-- RFC 6874 +local ZoneID = Cs((unreserved + _M.pct_encoded)^1) +local IPv6addrz = IPv6address * (P"%25" * ZoneID)^-1 / function(IPv6, zoneid) + IPv6:setzoneid(zoneid) + return IPv6 +end + +_M.IP_literal = P"[" * (IPv6addrz + IPvFuture) * P"]" +local IP_host = (_M.IP_literal + IPv4address) / tostring +local reg_name = Cs(( + unreserved / string.lower + + _M.pct_encoded / function(s) return s:sub(1,1) == "%" and s or string.lower(s) end + + _M.sub_delims +)^1) + Cc(nil) +_M.host = IP_host + reg_name + +_M.port = DIGIT^0 / tonumber -- 3.2.3 + +-- Path 3.3 +local pchar = unreserved + _M.pct_encoded + _M.sub_delims + S":@" +local segment = pchar^0 +_M.segment = Cs(segment) +local segment_nz = pchar^1 +local segment_nz_nc = (pchar - P":")^1 + +-- an empty path is nil instead of the empty string +local path_empty = Cc(nil) +local path_abempty = Cs((P"/" * segment)^1) + path_empty +local path_rootless = Cs(segment_nz * (P"/" * segment)^0) +local path_noscheme = Cs(segment_nz_nc * (P"/" * segment)^0) +local path_absolute = Cs(P"/" * (segment_nz * (P"/" * segment)^0)^-1) + +_M.query = Cs( ( pchar + S"/?" )^0 ) -- 3.4 +_M.fragment = _M.query -- 3.5 + +-- Put together with named captures +_M.authority = ( Cg(_M.userinfo, "userinfo") * P"@" )^-1 + * Cg(_M.host, "host") + * ( P":" * Cg(_M.port, "port") )^-1 + +local hier_part = P"//" * _M.authority * Cg (path_abempty, "path") + + Cg(path_absolute + path_rootless + path_empty, "path") + +_M.absolute_uri = Ct ( + ( Cg(_M.scheme, "scheme") * P":" ) + * hier_part + * ( P"?" * Cg(_M.query, "query"))^-1 +) + +_M.uri = Ct ( + ( Cg(_M.scheme, "scheme") * P":" ) + * hier_part + * ( P"?" * Cg(_M.query, "query"))^-1 + * ( P"#" * Cg(_M.fragment, "fragment"))^-1 +) + +_M.relative_part = P"//" * _M.authority * Cg(path_abempty, "path") + + Cg(path_absolute + path_noscheme + path_empty, "path") + +local relative_ref = Ct ( + _M.relative_part + * ( P"?" * Cg(_M.query, "query"))^-1 + * ( P"#" * Cg(_M.fragment, "fragment"))^-1 +) +_M.uri_reference = _M.uri + relative_ref + +_M.path = path_abempty + path_absolute + path_noscheme + path_rootless + path_empty + +-- Create a slightly more sane host pattern +-- scheme is optional +-- the "//" isn't required + -- if missing, the host needs to at least have a "." and end in two alpha characters +-- an authority is always required +local sane_host_char = unreserved / string.lower +local hostsegment = (sane_host_char - P".")^1 +local dns_entry = Cs ( ( hostsegment * P"." )^1 * ALPHA^2 ) +_M.sane_host = IP_host + dns_entry +_M.sane_authority = ( Cg(_M.userinfo, "userinfo") * P"@" )^-1 + * Cg(_M.sane_host, "host") + * ( P":" * Cg(_M.port, "port") )^-1 +local sane_hier_part = (P"//")^-1 * _M.sane_authority * Cg(path_absolute + path_empty, "path") +_M.sane_uri = Ct ( + ( Cg(_M.scheme, "scheme") * P":" )^-1 + * sane_hier_part + * ( P"?" * Cg(_M.query, "query"))^-1 + * ( P"#" * Cg(_M.fragment, "fragment"))^-1 +) + +return _M diff --git a/framework/lualib/3rd/lpeg_patterns/util.lua b/framework/lualib/3rd/lpeg_patterns/util.lua new file mode 100755 index 0000000..a612503 --- /dev/null +++ b/framework/lualib/3rd/lpeg_patterns/util.lua @@ -0,0 +1,55 @@ +local lpeg = require "lpeg" +local C = lpeg.C +local P = lpeg.P +local S = lpeg.S + +local function case_insensitive(str) + local patt = P(true) + for i=1, #str do + local c = str:sub(i, i) + patt = patt * S(c:upper() .. c:lower()) + end + return patt +end + +local function no_rich_capture(patt) + return C(patt) / function(a) return a end +end + +local function read_hex(hex_num) + return tonumber(hex_num, 16) +end + +local safe_tonumber do -- locale independent tonumber function + local tolocale + local function updatelocale() + local decpoint = string.format("%f", 0.5):match "[^05]+" + if decpoint == "." then + tolocale = function(str) + return str + end + else + tolocale = function(str) + str = str:gsub("%.", decpoint, 1) + return str + end + end + end + updatelocale() + safe_tonumber = function(str) + local num = tonumber(tolocale(str)) + if num then + return num + else + updatelocale() + return tonumber(tolocale(str)) + end + end +end + +return { + case_insensitive = case_insensitive; + no_rich_capture = no_rich_capture; + read_hex = read_hex; + safe_tonumber = safe_tonumber; +} diff --git a/framework/lualib/3rd/misc/bint.lua b/framework/lualib/3rd/misc/bint.lua new file mode 100644 index 0000000..404de27 --- /dev/null +++ b/framework/lualib/3rd/misc/bint.lua @@ -0,0 +1,1651 @@ +--[[-- +lua-bint - v0.4.1 - 17/Aug/2021 +Eduardo Bart - edub4rt@gmail.com +https://github.com/edubart/lua-bint + +Small portable arbitrary-precision integer arithmetic library in pure Lua for +computing with large integers. + +Different from most arbitrary-precision integer libraries in pure Lua out there this one +uses an array of lua integers as underlying data-type in its implementation instead of +using strings or large tables, this make it efficient for working with fixed width integers +and to make bitwise operations. + +## Design goals + +The main design goal of this library is to be small, correct, self contained and use few +resources while retaining acceptable performance and feature completeness. + +The library is designed to follow recent Lua integer semantics, this means that +integer overflow warps around, +signed integers are implemented using two-complement arithmetic rules, +integer division operations rounds towards minus infinity, +any mixed operations with float numbers promotes the value to a float, +and the usual division/power operation always promotes to floats. + +The library is designed to be possible to work with only unsigned integer arithmetic +when using the proper methods. + +All the lua arithmetic operators (+, -, *, //, /, %) and bitwise operators (&, |, ~, <<, >>) +are implemented as metamethods. + +The integer size must be fixed in advance and the library is designed to be more efficient when +working with integers of sizes between 64-4096 bits. If you need to work with really huge numbers +without size restrictions then use another library. This choice has been made to have more efficiency +in that specific size range. + +## Usage + +First on you should require the bint file including how many bits the bint module will work with, +by calling the returned function from the require, for example: + +```lua +local bint = require 'bint'(1024) +``` + +For more information about its arguments see @{newmodule}. +Then when you need create a bint, you can use one of the following functions: + +* @{bint.fromuinteger} (convert from lua integers, but read as unsigned integer) +* @{bint.frominteger} (convert from lua integers, preserving the sign) +* @{bint.frombase} (convert from arbitrary bases, like hexadecimal) +* @{bint.trunc} (convert from lua numbers, truncating the fractional part) +* @{bint.new} (convert from anything, asserts on invalid integers) +* @{bint.tobint} (convert from anything, returns nil on invalid integers) +* @{bint.parse} (convert from anything, returns a lua number as fallback) +* @{bint.zero} +* @{bint.one} +* `bint` + +You can also call `bint` as it is an alias to `bint.new`. +In doubt use @{bint.new} to create a new bint. + +Then you can use all the usual lua numeric operations on it, +all the arithmetic metamethods are implemented. +When you are done computing and need to get the result, +get the output from one of the following functions: + +* @{bint.touinteger} (convert to a lua integer, wraps around as an unsigned integer) +* @{bint.tointeger} (convert to a lua integer, wraps around, preserves the sign) +* @{bint.tonumber} (convert to lua float, losing precision) +* @{bint.tobase} (convert to a string in any base) +* @{bint.__tostring} (convert to a string in base 10) + +To output a very large integer with no loss you probably want to use @{bint.tobase} +or call `tostring` to get a string representation. + +## Precautions + +All library functions can be mixed with lua numbers, +this makes easy to mix operations between bints and lua numbers, +however the user should take care in some situations: + +* Don't mix integers and float operations if you want to work with integers only. +* Don't use the regular equal operator ('==') to compare values from this library, +unless you know in advance that both values are of the same primitive type, +otherwise it will always return false, use @{bint.eq} to be safe. +* Don't pass fractional numbers to functions that an integer is expected +* Don't mix operations between bint classes with different sizes as this is not supported, this +will throw assertions. +* Remember that casting back to lua integers or numbers precision can be lost. +* For dividing while preserving integers use the @{bint.__idiv} (the '//' operator). +* For doing power operation preserving integers use the @{bint.ipow} function. +* Configure the proper integer size you intend to work with, otherwise large integers may wrap around. + +]] + +-- Returns number of bits of the internal lua integer type. +local function luainteger_bitsize() + local n, i = -1, 0 + repeat + n, i = n >> 16, i + 16 + until n == 0 + return i +end + +local math_type = math.type +local math_floor = math.floor +local math_abs = math.abs +local math_ceil = math.ceil +local math_modf = math.modf +local math_mininteger = math.mininteger +local math_maxinteger = math.maxinteger +local math_max = math.max +local math_min = math.min +local string_format = string.format +local table_insert = table.insert +local table_concat = table.concat + +local memo = {} + +--- Create a new bint module representing integers of the desired bit size. +-- This is the returned function when `require 'bint'` is called. +-- @function newmodule +-- @param bits Number of bits for the integer representation, must be multiple of wordbits and +-- at least 64. +-- @param[opt] wordbits Number of the bits for the internal word, +-- defaults to half of Lua's integer size. +local function newmodule(bits, wordbits) + + local intbits = luainteger_bitsize() + bits = bits or 256 + wordbits = wordbits or (intbits // 2) + + -- Memoize bint modules + local memoindex = bits * 64 + wordbits + if memo[memoindex] then + return memo[memoindex] + end + + -- Validate + assert(bits % wordbits == 0, 'bitsize is not multiple of word bitsize') + assert(2 * wordbits <= intbits, 'word bitsize must be half of the lua integer bitsize') + assert(bits >= 64, 'bitsize must be >= 64') + + -- Create bint module + local bint = {} + bint.__index = bint + + --- Number of bits representing a bint instance. + bint.bits = bits + + -- Constants used internally + local BINT_BITS = bits + local BINT_WORDBITS = wordbits + local BINT_SIZE = BINT_BITS // BINT_WORDBITS + local BINT_WORDMAX = (1 << BINT_WORDBITS) - 1 + local BINT_WORDMSB = (1 << (BINT_WORDBITS - 1)) + local BINT_MATHMININTEGER, BINT_MATHMAXINTEGER + local BINT_MININTEGER + + --- Create a new bint with 0 value. + function bint.zero() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE do + x[i] = 0 + end + return x + end + local bint_zero = bint.zero + + --- Create a new bint with 1 value. + function bint.one() + local x = setmetatable({}, bint) + x[1] = 1 + for i = 2, BINT_SIZE do + x[i] = 0 + end + return x + end + local bint_one = bint.one + + -- Convert a value to a lua integer without losing precision. + local function tointeger(x) + x = tonumber(x) + local ty = math_type(x) + if ty == 'float' then + local floorx = math_floor(x) + if floorx == x then + x = floorx + ty = math_type(x) + end + end + if ty == 'integer' then + return x + end + end + + --- Create a bint from an unsigned integer. + -- Treats signed integers as an unsigned integer. + -- @param x A value to initialize from convertible to a lua integer. + -- @return A new bint or nil in case the input cannot be represented by an integer. + -- @see bint.frominteger + function bint.fromuinteger(x) + x = tointeger(x) + if x then + if x == 1 then + return bint_one() + elseif x == 0 then + return bint_zero() + end + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x & BINT_WORDMAX + x = x >> BINT_WORDBITS + end + return n + end + end + local bint_fromuinteger = bint.fromuinteger + + --- Create a bint from a signed integer. + -- @param x A value to initialize from convertible to a lua integer. + -- @return A new bint or nil in case the input cannot be represented by an integer. + -- @see bint.fromuinteger + function bint.frominteger(x) + x = tointeger(x) + if x then + if x == 1 then + return bint_one() + elseif x == 0 then + return bint_zero() + end + local neg = false + if x < 0 then + x = math_abs(x) + neg = true + end + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x & BINT_WORDMAX + x = x >> BINT_WORDBITS + end + if neg then + n:_unm() + end + return n + end + end + local bint_frominteger = bint.frominteger + + local basesteps = {} + + -- Compute the read step for frombase function + local function getbasestep(base) + local step = basesteps[base] + if step then + return step + end + step = 0 + local dmax = 1 + local limit = math_maxinteger // base + repeat + step = step + 1 + dmax = dmax * base + until dmax >= limit + basesteps[base] = step + return step + end + + -- Compute power with lua integers. + local function ipow(y, x, n) + if n == 1 then + return y * x + elseif n & 1 == 0 then -- even + return ipow(y, x * x, n // 2) + end + return ipow(x * y, x * x, (n - 1) // 2) + end + + --- Create a bint from a string of the desired base. + -- @param s The string to be converted from, + -- must have only alphanumeric and '+-' characters. + -- @param[opt] base Base that the number is represented, defaults to 10. + -- Must be at least 2 and at most 36. + -- @return A new bint or nil in case the conversion failed. + function bint.frombase(s, base) + if type(s) ~= 'string' then + return + end + base = base or 10 + if not (base >= 2 and base <= 36) then + -- number base is too large + return + end + local step = getbasestep(base) + if #s < step then + -- string is small, use tonumber (faster) + return bint_frominteger(tonumber(s, base)) + end + local sign, int = s:lower():match('^([+-]?)(%w+)$') + if not (sign and int) then + -- invalid integer string representation + return + end + local n = bint_zero() + for i = 1, #int, step do + local part = int:sub(i, i + step - 1) + local d = tonumber(part, base) + if not d then + -- invalid integer string representation + return + end + if i > 1 then + n = n * ipow(1, base, #part) + end + if d ~= 0 then + n:_add(d) + end + end + if sign == '-' then + n:_unm() + end + return n + end + local bint_frombase = bint.frombase + + --- Create a new bint from a value. + -- @param x A value convertible to a bint (string, number or another bint). + -- @return A new bint, guaranteed to be a new reference in case needed. + -- @raise An assert is thrown in case x is not convertible to a bint. + -- @see bint.tobint + -- @see bint.parse + function bint.new(x) + if getmetatable(x) ~= bint then + local ty = type(x) + if ty == 'number' then + return bint_frominteger(x) + elseif ty == 'string' then + return bint_frombase(x, 10) + end + error('value cannot be represented by a bint') + end + -- return a clone + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x[i] + end + return n + end + local bint_new = bint.new + + --- Convert a value to a bint if possible. + -- @param x A value to be converted (string, number or another bint). + -- @param[opt] clone A boolean that tells if a new bint reference should be returned. + -- Defaults to false. + -- @return A bint or nil in case the conversion failed. + -- @see bint.new + -- @see bint.parse + function bint.tobint(x, clone) + if getmetatable(x) == bint then + if not clone then + return x + end + -- return a clone + local n = setmetatable({}, bint) + for i = 1, BINT_SIZE do + n[i] = x[i] + end + return n + end + local ty = type(x) + if ty == 'number' then + return bint_frominteger(x) + elseif ty == 'string' then + return bint_frombase(x, 10) + end + end + local tobint = bint.tobint + + --- Convert a value to a bint if possible otherwise to a lua number. + -- Useful to prepare values that you are unsure if it's going to be an integer or float. + -- @param x A value to be converted (string, number or another bint). + -- @param[opt] clone A boolean that tells if a new bint reference should be returned. + -- Defaults to false. + -- @return A bint or a lua number or nil in case the conversion failed. + -- @see bint.new + -- @see bint.tobint + function bint.parse(x, clone) + local i = tobint(x, clone) + if i then + return i + end + return tonumber(x) + end + local bint_parse = bint.parse + + --- Convert a bint to an unsigned integer. + -- Note that large unsigned integers may be represented as negatives in lua integers. + -- Note that lua cannot represent values larger than 64 bits, + -- in that case integer values wrap around. + -- @param x A bint or a number to be converted into an unsigned integer. + -- @return An integer or nil in case the input cannot be represented by an integer. + -- @see bint.tointeger + function bint.touinteger(x) + if getmetatable(x) == bint then + local n = 0 + for i = 1, BINT_SIZE do + n = n | (x[i] << (BINT_WORDBITS * (i - 1))) + end + return n + end + return tointeger(x) + end + + --- Convert a bint to a signed integer. + -- It works by taking absolute values then applying the sign bit in case needed. + -- Note that lua cannot represent values larger than 64 bits, + -- in that case integer values wrap around. + -- @param x A bint or value to be converted into an unsigned integer. + -- @return An integer or nil in case the input cannot be represented by an integer. + -- @see bint.touinteger + function bint.tointeger(x) + if getmetatable(x) == bint then + local n = 0 + local neg = x:isneg() + if neg then + x = -x + end + for i = 1, BINT_SIZE do + n = n | (x[i] << (BINT_WORDBITS * (i - 1))) + end + if neg then + n = -n + end + return n + end + return tointeger(x) + end + local bint_tointeger = bint.tointeger + + local function bint_assert_tointeger(x) + x = bint_tointeger(x) + if not x then + error('value has no integer representation') + end + return x + end + + --- Convert a bint to a lua float in case integer would wrap around or lua integer otherwise. + -- Different from @{bint.tointeger} the operation does not wrap around integers, + -- but digits precision are lost in the process of converting to a float. + -- @param x A bint or value to be converted into a lua number. + -- @return A lua number or nil in case the input cannot be represented by a number. + -- @see bint.tointeger + function bint.tonumber(x) + if getmetatable(x) == bint then + if x <= BINT_MATHMAXINTEGER and x >= BINT_MATHMININTEGER then + return x:tointeger() + end + return tonumber(tostring(x)) + end + return tonumber(x) + end + local bint_tonumber = bint.tonumber + + -- Compute base letters to use in bint.tobase + local BASE_LETTERS = {} + do + for i = 1, 36 do + BASE_LETTERS[i - 1] = ('0123456789abcdefghijklmnopqrstuvwxyz'):sub(i, i) + end + end + + --- Convert a bint to a string in the desired base. + -- @param x The bint to be converted from. + -- @param[opt] base Base to be represented, defaults to 10. + -- Must be at least 2 and at most 36. + -- @param[opt] unsigned Whether to output as an unsigned integer. + -- Defaults to false for base 10 and true for others. + -- When unsigned is false the symbol '-' is prepended in negative values. + -- @return A string representing the input. + -- @raise An assert is thrown in case the base is invalid. + function bint.tobase(x, base, unsigned) + x = tobint(x) + if not x then + -- x is a fractional float or something else + return + end + base = base or 10 + if not (base >= 2 and base <= 36) then + -- number base is too large + return + end + if unsigned == nil then + unsigned = base ~= 10 + end + local isxneg = x:isneg() + if (base == 10 and not unsigned) or (base == 16 and unsigned and not isxneg) then + if x <= BINT_MATHMAXINTEGER and x >= BINT_MATHMININTEGER then + -- integer is small, use tostring or string.format (faster) + local n = x:tointeger() + if base == 10 then + return tostring(n) + elseif unsigned then + return string_format('%x', n) + end + end + end + local ss = {} + local neg = not unsigned and isxneg + x = neg and x:abs() or bint_new(x) + local xiszero = x:iszero() + if xiszero then + return '0' + end + -- calculate basepow + local step = 0 + local basepow = 1 + local limit = (BINT_WORDMSB - 1) // base + repeat + step = step + 1 + basepow = basepow * base + until basepow >= limit + -- serialize base digits + local size = BINT_SIZE + local xd, carry, d + repeat + -- single word division + carry = 0 + xiszero = true + for i = size, 1, -1 do + carry = carry | x[i] + d, xd = carry // basepow, carry % basepow + if xiszero and d ~= 0 then + size = i + xiszero = false + end + x[i] = d + carry = xd << BINT_WORDBITS + end + -- digit division + for _ = 1, step do + xd, d = xd // base, xd % base + if xiszero and xd == 0 and d == 0 then + -- stop on leading zeros + break + end + table_insert(ss, 1, BASE_LETTERS[d]) + end + until xiszero + if neg then + table_insert(ss, 1, '-') + end + return table_concat(ss) + end + + local function bint_assert_convert(x) + return assert(tobint(x), 'value has not integer representation') + end + + --- Check if a number is 0 considering bints. + -- @param x A bint or a lua number. + function bint.iszero(x) + if getmetatable(x) == bint then + for i = 1, BINT_SIZE do + if x[i] ~= 0 then + return false + end + end + return true + end + return x == 0 + end + + --- Check if a number is 1 considering bints. + -- @param x A bint or a lua number. + function bint.isone(x) + if getmetatable(x) == bint then + if x[1] ~= 1 then + return false + end + for i = 2, BINT_SIZE do + if x[i] ~= 0 then + return false + end + end + return true + end + return x == 1 + end + + --- Check if a number is -1 considering bints. + -- @param x A bint or a lua number. + function bint.isminusone(x) + if getmetatable(x) == bint then + for i = 1, BINT_SIZE do + if x[i] ~= BINT_WORDMAX then + return false + end + end + return true + end + return x == -1 + end + local bint_isminusone = bint.isminusone + + --- Check if the input is a bint. + -- @param x Any lua value. + function bint.isbint(x) + return getmetatable(x) == bint + end + + --- Check if the input is a lua integer or a bint. + -- @param x Any lua value. + function bint.isintegral(x) + return getmetatable(x) == bint or math_type(x) == 'integer' + end + + --- Check if the input is a bint or a lua number. + -- @param x Any lua value. + function bint.isnumeric(x) + return getmetatable(x) == bint or type(x) == 'number' + end + + --- Get the number type of the input (bint, integer or float). + -- @param x Any lua value. + -- @return Returns "bint" for bints, "integer" for lua integers, + -- "float" from lua floats or nil otherwise. + function bint.type(x) + if getmetatable(x) == bint then + return 'bint' + end + return math_type(x) + end + + --- Check if a number is negative considering bints. + -- Zero is guaranteed to never be negative for bints. + -- @param x A bint or a lua number. + function bint.isneg(x) + if getmetatable(x) == bint then + return x[BINT_SIZE] & BINT_WORDMSB ~= 0 + end + return x < 0 + end + local bint_isneg = bint.isneg + + --- Check if a number is positive considering bints. + -- @param x A bint or a lua number. + function bint.ispos(x) + if getmetatable(x) == bint then + return not x:isneg() and not x:iszero() + end + return x > 0 + end + + --- Check if a number is even considering bints. + -- @param x A bint or a lua number. + function bint.iseven(x) + if getmetatable(x) == bint then + return x[1] & 1 == 0 + end + return math_abs(x) % 2 == 0 + end + + --- Check if a number is odd considering bints. + -- @param x A bint or a lua number. + function bint.isodd(x) + if getmetatable(x) == bint then + return x[1] & 1 == 1 + end + return math_abs(x) % 2 == 1 + end + + --- Create a new bint with the maximum possible integer value. + function bint.maxinteger() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE - 1 do + x[i] = BINT_WORDMAX + end + x[BINT_SIZE] = BINT_WORDMAX ~ BINT_WORDMSB + return x + end + + --- Create a new bint with the minimum possible integer value. + function bint.mininteger() + local x = setmetatable({}, bint) + for i = 1, BINT_SIZE - 1 do + x[i] = 0 + end + x[BINT_SIZE] = BINT_WORDMSB + return x + end + + --- Bitwise left shift a bint in one bit (in-place). + function bint:_shlone() + local wordbitsm1 = BINT_WORDBITS - 1 + for i = BINT_SIZE, 2, -1 do + self[i] = ((self[i] << 1) | (self[i - 1] >> wordbitsm1)) & BINT_WORDMAX + end + self[1] = (self[1] << 1) & BINT_WORDMAX + return self + end + + --- Bitwise right shift a bint in one bit (in-place). + function bint:_shrone() + local wordbitsm1 = BINT_WORDBITS - 1 + for i = 1, BINT_SIZE - 1 do + self[i] = ((self[i] >> 1) | (self[i + 1] << wordbitsm1)) & BINT_WORDMAX + end + self[BINT_SIZE] = self[BINT_SIZE] >> 1 + return self + end + + -- Bitwise left shift words of a bint (in-place). Used only internally. + function bint:_shlwords(n) + for i = BINT_SIZE, n + 1, -1 do + self[i] = self[i - n] + end + for i = 1, n do + self[i] = 0 + end + return self + end + + -- Bitwise right shift words of a bint (in-place). Used only internally. + function bint:_shrwords(n) + if n < BINT_SIZE then + for i = 1, BINT_SIZE - n do + self[i] = self[i + n] + end + for i = BINT_SIZE - n + 1, BINT_SIZE do + self[i] = 0 + end + else + for i = 1, BINT_SIZE do + self[i] = 0 + end + end + return self + end + + --- Increment a bint by one (in-place). + function bint:_inc() + for i = 1, BINT_SIZE do + local tmp = self[i] + local v = (tmp + 1) & BINT_WORDMAX + self[i] = v + if v > tmp then + break + end + end + return self + end + + --- Increment a number by one considering bints. + -- @param x A bint or a lua number to increment. + function bint.inc(x) + local ix = tobint(x, true) + if ix then + return ix:_inc() + end + return x + 1 + end + + --- Decrement a bint by one (in-place). + function bint:_dec() + for i = 1, BINT_SIZE do + local tmp = self[i] + local v = (tmp - 1) & BINT_WORDMAX + self[i] = v + if not (v > tmp) then + break + end + end + return self + end + + --- Decrement a number by one considering bints. + -- @param x A bint or a lua number to decrement. + function bint.dec(x) + local ix = tobint(x, true) + if ix then + return ix:_dec() + end + return x - 1 + end + + --- Assign a bint to a new value (in-place). + -- @param y A value to be copied from. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_assign(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = y[i] + end + return self + end + + --- Take absolute of a bint (in-place). + function bint:_abs() + if self:isneg() then + self:_unm() + end + return self + end + + --- Take absolute of a number considering bints. + -- @param x A bint or a lua number to take the absolute. + function bint.abs(x) + local ix = tobint(x, true) + if ix then + return ix:_abs() + end + return math_abs(x) + end + local bint_abs = bint.abs + + --- Take the floor of a number considering bints. + -- @param x A bint or a lua number to perform the floor operation. + function bint.floor(x) + if getmetatable(x) == bint then + return bint_new(x) + end + return bint_new(math_floor(tonumber(x))) + end + + --- Take ceil of a number considering bints. + -- @param x A bint or a lua number to perform the ceil operation. + function bint.ceil(x) + if getmetatable(x) == bint then + return bint_new(x) + end + return bint_new(math_ceil(tonumber(x))) + end + + --- Wrap around bits of an integer (discarding left bits) considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of right bits to preserve. + function bint.bwrap(x, y) + x = bint_assert_convert(x) + if y <= 0 then + return bint_zero() + elseif y < BINT_BITS then + return x & (bint_one() << y):_dec() + end + return bint_new(x) + end + + --- Rotate left integer x by y bits considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of bits to rotate. + function bint.brol(x, y) + x, y = bint_assert_convert(x), bint_assert_tointeger(y) + if y > 0 then + return (x << y) | (x >> (BINT_BITS - y)) + elseif y < 0 then + if y ~= math_mininteger then + return x:bror(-y) + else + x:bror(-(y + 1)) + x:bror(1) + end + end + return x + end + + --- Rotate right integer x by y bits considering bints. + -- @param x A bint or a lua integer. + -- @param y Number of bits to rotate. + function bint.bror(x, y) + x, y = bint_assert_convert(x), bint_assert_tointeger(y) + if y > 0 then + return (x >> y) | (x << (BINT_BITS - y)) + elseif y < 0 then + if y ~= math_mininteger then + return x:brol(-y) + else + x:brol(-(y + 1)) + x:brol(1) + end + end + return x + end + + --- Truncate a number to a bint. + -- Floats numbers are truncated, that is, the fractional port is discarded. + -- @param x A number to truncate. + -- @return A new bint or nil in case the input does not fit in a bint or is not a number. + function bint.trunc(x) + if getmetatable(x) ~= bint then + x = tonumber(x) + if x then + local ty = math_type(x) + if ty == 'float' then + -- truncate to integer + x = math_modf(x) + end + return bint_frominteger(x) + end + return + end + return bint_new(x) + end + + --- Take maximum between two numbers considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + -- @return A bint or a lua number. Guarantees to return a new bint for integer values. + function bint.max(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return bint_new(ix > iy and ix or iy) + end + return bint_parse(math_max(x, y)) + end + + --- Take minimum between two numbers considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + -- @return A bint or a lua number. Guarantees to return a new bint for integer values. + function bint.min(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return bint_new(ix < iy and ix or iy) + end + return bint_parse(math_min(x, y)) + end + + --- Add an integer to a bint (in-place). + -- @param y An integer to be added. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_add(y) + y = bint_assert_convert(y) + local carry = 0 + for i = 1, BINT_SIZE do + local tmp = self[i] + y[i] + carry + carry = tmp >> BINT_WORDBITS + self[i] = tmp & BINT_WORDMAX + end + return self + end + + --- Add two numbers considering bints. + -- @param x A bint or a lua number to be added. + -- @param y A bint or a lua number to be added. + function bint.__add(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = setmetatable({}, bint) + local carry = 0 + for i = 1, BINT_SIZE do + local tmp = ix[i] + iy[i] + carry + carry = tmp >> BINT_WORDBITS + z[i] = tmp & BINT_WORDMAX + end + return z + end + return bint_tonumber(x) + bint_tonumber(y) + end + + --- Subtract an integer from a bint (in-place). + -- @param y An integer to subtract. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_sub(y) + y = bint_assert_convert(y) + local borrow = 0 + local wordmaxp1 = BINT_WORDMAX + 1 + for i = 1, BINT_SIZE do + local res = self[i] + wordmaxp1 - y[i] - borrow + self[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + return self + end + + --- Subtract two numbers considering bints. + -- @param x A bint or a lua number to be subtracted from. + -- @param y A bint or a lua number to subtract. + function bint.__sub(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = setmetatable({}, bint) + local borrow = 0 + local wordmaxp1 = BINT_WORDMAX + 1 + for i = 1, BINT_SIZE do + local res = ix[i] + wordmaxp1 - iy[i] - borrow + z[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + return z + end + return bint_tonumber(x) - bint_tonumber(y) + end + + --- Multiply two numbers considering bints. + -- @param x A bint or a lua number to multiply. + -- @param y A bint or a lua number to multiply. + function bint.__mul(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local z = bint_zero() + local sizep1 = BINT_SIZE + 1 + local s = sizep1 + local e = 0 + for i = 1, BINT_SIZE do + if ix[i] ~= 0 or iy[i] ~= 0 then + e = math_max(e, i) + s = math_min(s, i) + end + end + for i = s, e do + for j = s, math_min(sizep1 - i, e) do + local a = ix[i] * iy[j] + if a ~= 0 then + local carry = 0 + for k = i + j - 1, BINT_SIZE do + local tmp = z[k] + (a & BINT_WORDMAX) + carry + carry = tmp >> BINT_WORDBITS + z[k] = tmp & BINT_WORDMAX + a = a >> BINT_WORDBITS + end + end + end + end + return z + end + return bint_tonumber(x) * bint_tonumber(y) + end + + --- Check if bints are equal. + -- @param x A bint to compare. + -- @param y A bint to compare. + function bint.__eq(x, y) + for i = 1, BINT_SIZE do + if x[i] ~= y[i] then + return false + end + end + return true + end + + --- Check if numbers are equal considering bints. + -- @param x A bint or lua number to compare. + -- @param y A bint or lua number to compare. + function bint.eq(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + return ix == iy + end + return x == y + end + local bint_eq = bint.eq + + local function findleftbit(x) + for i = BINT_SIZE, 1, -1 do + local v = x[i] + if v ~= 0 then + local j = 0 + repeat + v = v >> 1 + j = j + 1 + until v == 0 + return (i - 1) * BINT_WORDBITS + j - 1, i + end + end + end + + -- Single word division modulus + local function sudivmod(nume, deno) + local rema + local carry = 0 + for i = BINT_SIZE, 1, -1 do + carry = carry | nume[i] + nume[i] = carry // deno + rema = carry % deno + carry = rema << BINT_WORDBITS + end + return rema + end + + --- Perform unsigned division and modulo operation between two integers considering bints. + -- This is effectively the same of @{bint.udiv} and @{bint.umod}. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The quotient following the remainder, both bints. + -- @raise Asserts on attempt to divide by zero + -- or if inputs are not convertible to integers. + -- @see bint.udiv + -- @see bint.umod + function bint.udivmod(x, y) + local nume = bint_new(x) + local deno = bint_assert_convert(y) + -- compute if high bits of denominator are all zeros + local ishighzero = true + for i = 2, BINT_SIZE do + if deno[i] ~= 0 then + ishighzero = false + break + end + end + if ishighzero then + -- try to divide by a single word (optimization) + local low = deno[1] + assert(low ~= 0, 'attempt to divide by zero') + if low == 1 then + -- denominator is one + return nume, bint_zero() + elseif low <= (BINT_WORDMSB - 1) then + -- can do single word division + local rema = sudivmod(nume, low) + return nume, bint_fromuinteger(rema) + end + end + if nume:ult(deno) then + -- denominator is greater than numerator + return bint_zero(), nume + end + -- align leftmost digits in numerator and denominator + local denolbit = findleftbit(deno) + local numelbit, numesize = findleftbit(nume) + local bit = numelbit - denolbit + deno = deno << bit + local wordmaxp1 = BINT_WORDMAX + 1 + local wordbitsm1 = BINT_WORDBITS - 1 + local denosize = numesize + local quot = bint_zero() + while bit >= 0 do + -- compute denominator <= numerator + local le = true + local size = math_max(numesize, denosize) + for i = size, 1, -1 do + local a, b = deno[i], nume[i] + if a ~= b then + le = a < b + break + end + end + -- if the portion of the numerator above the denominator is greater or equal than to the denominator + if le then + -- subtract denominator from the portion of the numerator + local borrow = 0 + for i = 1, size do + local res = nume[i] + wordmaxp1 - deno[i] - borrow + nume[i] = res & BINT_WORDMAX + borrow = (res >> BINT_WORDBITS) ~ 1 + end + -- concatenate 1 to the right bit of the quotient + local i = (bit // BINT_WORDBITS) + 1 + quot[i] = quot[i] | (1 << (bit % BINT_WORDBITS)) + end + -- shift right the denominator in one bit + for i = 1, denosize - 1 do + deno[i] = ((deno[i] >> 1) | (deno[i + 1] << wordbitsm1)) & BINT_WORDMAX + end + local lastdenoword = deno[denosize] >> 1 + deno[denosize] = lastdenoword + -- recalculate denominator size (optimization) + if lastdenoword == 0 then + while deno[denosize] == 0 do + denosize = denosize - 1 + end + if denosize == 0 then + break + end + end + -- decrement current set bit for the quotient + bit = bit - 1 + end + -- the remaining numerator is the remainder + return quot, nume + end + local bint_udivmod = bint.udivmod + + --- Perform unsigned division between two integers considering bints. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The quotient, a bint. + -- @raise Asserts on attempt to divide by zero + -- or if inputs are not convertible to integers. + function bint.udiv(x, y) + return (bint_udivmod(x, y)) + end + + --- Perform unsigned integer modulo operation between two integers considering bints. + -- @param x The numerator, must be a bint or a lua integer. + -- @param y The denominator, must be a bint or a lua integer. + -- @return The remainder, a bint. + -- @raise Asserts on attempt to divide by zero + -- or if the inputs are not convertible to integers. + function bint.umod(x, y) + local _, rema = bint_udivmod(x, y) + return rema + end + local bint_umod = bint.umod + + --- Perform integer truncate division and modulo operation between two numbers considering bints. + -- This is effectively the same of @{bint.tdiv} and @{bint.tmod}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient following the remainder, both bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + -- @see bint.tdiv + -- @see bint.tmod + function bint.tdivmod(x, y) + local ax, ay = bint_abs(x), bint_abs(y) + local ix, iy = tobint(ax), tobint(ay) + local quot, rema + if ix and iy then + assert(not (bint_eq(x, BINT_MININTEGER) and bint_isminusone(y)), 'division overflow') + quot, rema = bint_udivmod(ix, iy) + else + quot, rema = ax // ay, ax % ay + end + local isxneg, isyneg = bint_isneg(x), bint_isneg(y) + if isxneg ~= isyneg then + quot = -quot + end + if isxneg then + rema = -rema + end + return quot, rema + end + local bint_tdivmod = bint.tdivmod + + --- Perform truncate division between two numbers considering bints. + -- Truncate division is a division that rounds the quotient towards zero. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + function bint.tdiv(x, y) + return (bint_tdivmod(x, y)) + end + + --- Perform integer truncate modulo operation between two numbers considering bints. + -- The operation is defined as the remainder of the truncate division + -- (division that rounds the quotient towards zero). + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The remainder, a bint or lua number. + -- @raise Asserts on attempt to divide by zero or on division overflow. + function bint.tmod(x, y) + local _, rema = bint_tdivmod(x, y) + return rema + end + + --- Perform integer floor division and modulo operation between two numbers considering bints. + -- This is effectively the same of @{bint.__idiv} and @{bint.__mod}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient following the remainder, both bint or lua number. + -- @raise Asserts on attempt to divide by zero. + -- @see bint.__idiv + -- @see bint.__mod + function bint.idivmod(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local isnumeneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local isdenoneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if isnumeneg then + ix = -ix + end + if isdenoneg then + iy = -iy + end + local quot, rema = bint_udivmod(ix, iy) + if isnumeneg ~= isdenoneg then + quot:_unm() + -- round quotient towards minus infinity + if not rema:iszero() then + quot:_dec() + -- adjust the remainder + if isnumeneg and not isdenoneg then + rema:_unm():_add(y) + elseif isdenoneg and not isnumeneg then + rema:_add(y) + end + end + elseif isnumeneg then + -- adjust the remainder + rema:_unm() + end + return quot, rema + end + local nx, ny = bint_tonumber(x), bint_tonumber(y) + return nx // ny, nx % ny + end + local bint_idivmod = bint.idivmod + + --- Perform floor division between two numbers considering bints. + -- Floor division is a division that rounds the quotient towards minus infinity, + -- resulting in the floor of the division of its operands. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a bint or lua number. + -- @raise Asserts on attempt to divide by zero. + function bint.__idiv(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local isnumeneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local isdenoneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if isnumeneg then + ix = -ix + end + if isdenoneg then + iy = -iy + end + local quot, rema = bint_udivmod(ix, iy) + if isnumeneg ~= isdenoneg then + quot:_unm() + -- round quotient towards minus infinity + if not rema:iszero() then + quot:_dec() + end + end + return quot, rema + end + return bint_tonumber(x) // bint_tonumber(y) + end + + --- Perform division between two numbers considering bints. + -- This always casts inputs to floats, for integer division only use @{bint.__idiv}. + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The quotient, a lua number. + function bint.__div(x, y) + return bint_tonumber(x) / bint_tonumber(y) + end + + --- Perform integer floor modulo operation between two numbers considering bints. + -- The operation is defined as the remainder of the floor division + -- (division that rounds the quotient towards minus infinity). + -- @param x The numerator, a bint or lua number. + -- @param y The denominator, a bint or lua number. + -- @return The remainder, a bint or lua number. + -- @raise Asserts on attempt to divide by zero. + function bint.__mod(x, y) + local _, rema = bint_idivmod(x, y) + return rema + end + + --- Perform integer power between two integers considering bints. + -- If y is negative then pow is performed as an unsigned integer. + -- @param x The base, an integer. + -- @param y The exponent, an integer. + -- @return The result of the pow operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__pow + -- @see bint.upowmod + function bint.ipow(x, y) + y = bint_assert_convert(y) + if y:iszero() then + return bint_one() + elseif y:isone() then + return bint_new(x) + end + -- compute exponentiation by squaring + x, y = bint_new(x), bint_new(y) + local z = bint_one() + repeat + if y:iseven() then + x = x * x + y:_shrone() + else + z = x * z + x = x * x + y:_dec():_shrone() + end + until y:isone() + return x * z + end + + --- Perform integer power between two unsigned integers over a modulus considering bints. + -- @param x The base, an integer. + -- @param y The exponent, an integer. + -- @param m The modulus, an integer. + -- @return The result of the pow operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__pow + -- @see bint.ipow + function bint.upowmod(x, y, m) + m = bint_assert_convert(m) + if m:isone() then + return bint_zero() + end + x, y = bint_new(x), bint_new(y) + local z = bint_one() + x = bint_umod(x, m) + while not y:iszero() do + if y:isodd() then + z = bint_umod(z * x, m) + end + y:_shrone() + x = bint_umod(x * x, m) + end + return z + end + + --- Perform numeric power between two numbers considering bints. + -- This always casts inputs to floats, for integer power only use @{bint.ipow}. + -- @param x The base, a bint or lua number. + -- @param y The exponent, a bint or lua number. + -- @return The result of the pow operation, a lua number. + -- @see bint.ipow + function bint.__pow(x, y) + return bint_tonumber(x) ^ bint_tonumber(y) + end + + --- Bitwise left shift integers considering bints. + -- @param x An integer to perform the bitwise shift. + -- @param y An integer with the number of bits to shift. + -- @return The result of shift operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__shl(x, y) + x, y = bint_new(x), bint_assert_tointeger(y) + if y == math_mininteger or math_abs(y) >= BINT_BITS then + return bint_zero() + end + if y < 0 then + return x >> -y + end + local nvals = y // BINT_WORDBITS + if nvals ~= 0 then + x:_shlwords(nvals) + y = y - nvals * BINT_WORDBITS + end + if y ~= 0 then + local wordbitsmy = BINT_WORDBITS - y + for i = BINT_SIZE, 2, -1 do + x[i] = ((x[i] << y) | (x[i - 1] >> wordbitsmy)) & BINT_WORDMAX + end + x[1] = (x[1] << y) & BINT_WORDMAX + end + return x + end + + --- Bitwise right shift integers considering bints. + -- @param x An integer to perform the bitwise shift. + -- @param y An integer with the number of bits to shift. + -- @return The result of shift operation, a bint. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__shr(x, y) + x, y = bint_new(x), bint_assert_tointeger(y) + if y == math_mininteger or math_abs(y) >= BINT_BITS then + return bint_zero() + end + if y < 0 then + return x << -y + end + local nvals = y // BINT_WORDBITS + if nvals ~= 0 then + x:_shrwords(nvals) + y = y - nvals * BINT_WORDBITS + end + if y ~= 0 then + local wordbitsmy = BINT_WORDBITS - y + for i = 1, BINT_SIZE - 1 do + x[i] = ((x[i] >> y) | (x[i + 1] << wordbitsmy)) & BINT_WORDMAX + end + x[BINT_SIZE] = x[BINT_SIZE] >> y + end + return x + end + + --- Bitwise AND bints (in-place). + -- @param y An integer to perform bitwise AND. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_band(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i] & y[i] + end + return self + end + + --- Bitwise AND two integers considering bints. + -- @param x An integer to perform bitwise AND. + -- @param y An integer to perform bitwise AND. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__band(x, y) + return bint_new(x):_band(y) + end + + --- Bitwise OR bints (in-place). + -- @param y An integer to perform bitwise OR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_bor(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i] | y[i] + end + return self + end + + --- Bitwise OR two integers considering bints. + -- @param x An integer to perform bitwise OR. + -- @param y An integer to perform bitwise OR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bor(x, y) + return bint_new(x):_bor(y) + end + + --- Bitwise XOR bints (in-place). + -- @param y An integer to perform bitwise XOR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint:_bxor(y) + y = bint_assert_convert(y) + for i = 1, BINT_SIZE do + self[i] = self[i] ~ y[i] + end + return self + end + + --- Bitwise XOR two integers considering bints. + -- @param x An integer to perform bitwise XOR. + -- @param y An integer to perform bitwise XOR. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bxor(x, y) + return bint_new(x):_bxor(y) + end + + --- Bitwise NOT a bint (in-place). + function bint:_bnot() + for i = 1, BINT_SIZE do + self[i] = (~self[i]) & BINT_WORDMAX + end + return self + end + + --- Bitwise NOT a bint. + -- @param x An integer to perform bitwise NOT. + -- @raise Asserts in case inputs are not convertible to integers. + function bint.__bnot(x) + local y = setmetatable({}, bint) + for i = 1, BINT_SIZE do + y[i] = (~x[i]) & BINT_WORDMAX + end + return y + end + + --- Negate a bint (in-place). This effectively applies two's complements. + function bint:_unm() + return self:_bnot():_inc() + end + + --- Negate a bint. This effectively applies two's complements. + -- @param x A bint to perform negation. + function bint.__unm(x) + return (~x):_inc() + end + + --- Compare if integer x is less than y considering bints (unsigned version). + -- @param x Left integer to compare. + -- @param y Right integer to compare. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__lt + function bint.ult(x, y) + x, y = bint_assert_convert(x), bint_assert_convert(y) + for i = BINT_SIZE, 1, -1 do + local a, b = x[i], y[i] + if a ~= b then + return a < b + end + end + return false + end + + --- Compare if bint x is less or equal than y considering bints (unsigned version). + -- @param x Left integer to compare. + -- @param y Right integer to compare. + -- @raise Asserts in case inputs are not convertible to integers. + -- @see bint.__le + function bint.ule(x, y) + x, y = bint_assert_convert(x), bint_assert_convert(y) + for i = BINT_SIZE, 1, -1 do + local a, b = x[i], y[i] + if a ~= b then + return a < b + end + end + return true + end + + --- Compare if number x is less than y considering bints and signs. + -- @param x Left value to compare, a bint or lua number. + -- @param y Right value to compare, a bint or lua number. + -- @see bint.ult + function bint.__lt(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local xneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local yneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if xneg == yneg then + for i = BINT_SIZE, 1, -1 do + local a, b = ix[i], iy[i] + if a ~= b then + return a < b + end + end + return false + end + return xneg and not yneg + end + return bint_tonumber(x) < bint_tonumber(y) + end + + --- Compare if number x is less or equal than y considering bints and signs. + -- @param x Left value to compare, a bint or lua number. + -- @param y Right value to compare, a bint or lua number. + -- @see bint.ule + function bint.__le(x, y) + local ix, iy = tobint(x), tobint(y) + if ix and iy then + local xneg = ix[BINT_SIZE] & BINT_WORDMSB ~= 0 + local yneg = iy[BINT_SIZE] & BINT_WORDMSB ~= 0 + if xneg == yneg then + for i = BINT_SIZE, 1, -1 do + local a, b = ix[i], iy[i] + if a ~= b then + return a < b + end + end + return true + end + return xneg and not yneg + end + return bint_tonumber(x) <= bint_tonumber(y) + end + + --- Convert a bint to a string on base 10. + -- @see bint.tobase + function bint:__tostring() + return self:tobase(10) + end + + -- Allow creating bints by calling bint itself + setmetatable(bint, { + __call = function(_, x) + return bint_new(x) + end, + }) + + BINT_MATHMININTEGER, BINT_MATHMAXINTEGER = bint_new(math.mininteger), bint_new(math.maxinteger) + BINT_MININTEGER = bint.mininteger() + memo[memoindex] = bint + + return bint + +end + +return newmodule diff --git a/framework/lualib/3rd/misc/mlib.lua b/framework/lualib/3rd/misc/mlib.lua new file mode 100644 index 0000000..4de6775 --- /dev/null +++ b/framework/lualib/3rd/misc/mlib.lua @@ -0,0 +1,1502 @@ +local unpack = table.unpack or unpack + +-- Used to handle variable-argument functions and whether they are passed as func{ table } or func( unpack( table ) ) +local function checkInput(...) + local input = {} + if type(...) ~= 'table' then + input = {...} + else + input = ... + end + return input +end + +-- Deals with floats / verify false false values. This can happen because of significant figures. +local function checkFuzzy(number1, number2) + return (number1 - .00001 <= number2 and number2 <= number1 + .00001) +end + +-- Remove multiple occurrences from a table. +local function removeDuplicatePairs(tab) + for index1 = #tab, 1, -1 do + local first = tab[index1] + for index2 = #tab, 1, -1 do + local second = tab[index2] + if index1 ~= index2 then + if type(first[1]) == 'number' and type(second[1]) == 'number' and type(first[2]) == 'number' and + type(second[2]) == 'number' then + if checkFuzzy(first[1], second[1]) and checkFuzzy(first[2], second[2]) then + table.remove(tab, index1) + end + elseif first[1] == second[1] and first[2] == second[2] then + table.remove(tab, index1) + end + end + end + end + return tab +end + +local function removeDuplicates4Points(tab) + for index1 = #tab, 1, -1 do + local first = tab[index1] + for index2 = #tab, 1, -1 do + local second = tab[index2] + if index1 ~= index2 then + if type(first[1]) ~= type(second[1]) then + return false + end + if type(first[2]) == 'number' and type(second[2]) == 'number' and type(first[3]) == 'number' and + type(second[3]) == 'number' then + if checkFuzzy(first[2], second[2]) and checkFuzzy(first[3], second[3]) then + table.remove(tab, index1) + end + elseif checkFuzzy(first[1], second[1]) and checkFuzzy(first[2], second[2]) and + checkFuzzy(first[3], second[3]) then + table.remove(tab, index1) + end + end + end + end + return tab +end + +-- Add points to the table. +local function addPoints(tab, x, y) + tab[#tab + 1] = x + tab[#tab + 1] = y +end + +-- Like removeDuplicatePairs but specifically for numbers in a flat table +local function removeDuplicatePointsFlat(tab) + for i = #tab, 1 - 2 do + for ii = #tab - 2, 3, -2 do + if i ~= ii then + local x1, y1 = tab[i], tab[i + 1] + local x2, y2 = tab[ii], tab[ii + 1] + if checkFuzzy(x1, x2) and checkFuzzy(y1, y2) then + table.remove(tab, ii); + table.remove(tab, ii + 1) + end + end + end + end + return tab +end + +-- Check if input is actually a number +local function validateNumber(n) + if type(n) ~= 'number' then + return false + elseif n ~= n then + return false -- nan + elseif math.abs(n) == math.huge then + return false + else + return true + end +end + +local function cycle(tab, index) + return tab[(index - 1) % #tab + 1] +end + +local function getGreatestPoint(points, offset) + offset = offset or 1 + local start = 2 - offset + local greatest = points[start] + local least = points[start] + for i = 2, #points / 2 do + i = i * 2 - offset + if points[i] > greatest then + greatest = points[i] + end + if points[i] < least then + least = points[i] + end + end + return greatest, least +end + +local function isWithinBounds(min, num, max) + return num >= min and num <= max +end + +local function distance2(x1, y1, x2, y2) -- Faster since it does not use math.sqrt + local dx, dy = x1 - x2, y1 - y2 + return dx * dx + dy * dy +end -- }}} + +-- Points -------------------------------------- {{{ +local function rotatePoint(x, y, rotation, ox, oy) + ox, oy = ox or 0, oy or 0 + return (x - ox) * math.cos(rotation) + ox - (y - oy) * math.sin(rotation), + (x - ox) * math.sin(rotation) + (y - oy) * math.cos(rotation) + oy +end + +local function scalePoint(x, y, scale, ox, oy) + ox, oy = ox or 0, oy or 0 + return (x - ox) * scale + ox, (y - oy) * scale + oy +end + +local function polarToCartesian(radius, theta, offsetRadius, offsetTheta) + local ox, oy = 0, 0 + if offsetRadius and offsetTheta then + ox, oy = polarToCartesian(offsetRadius, offsetTheta) + end + local x = radius * math.cos(theta) + local y = radius * math.sin(theta) + return x + ox, y + oy +end + +local function cartesianToPolar(x, y, ox, oy) + x, y = x - (ox or 0), y - (oy or 0) + local theta = math.atan2(y, x) + -- Convert to absolute angle + theta = theta > 0 and theta or theta + 2 * math.pi + local radius = math.sqrt(x ^ 2 + y ^ 2) + return radius, theta +end +-- }}} + +-- Lines --------------------------------------- {{{ +-- Returns the length of a line. +local function getLength(x1, y1, x2, y2) + local dx, dy = x1 - x2, y1 - y2 + return math.sqrt(dx * dx + dy * dy) +end + +-- Gives the midpoint of a line. +local function getMidpoint(x1, y1, x2, y2) + return (x1 + x2) / 2, (y1 + y2) / 2 +end + +-- Gives the slope of a line. +local function getSlope(x1, y1, x2, y2) + if checkFuzzy(x1, x2) then + return false + end -- Technically it's undefined, but this is easier to program. + return (y1 - y2) / (x1 - x2) +end + +-- Gives the perpendicular slope of a line. +-- x1, y1, x2, y2 +-- slope +local function getPerpendicularSlope(...) + local input = checkInput(...) + local slope + + if #input ~= 1 then + slope = getSlope(unpack(input)) + else + slope = unpack(input) + end + + if not slope then + return 0 -- Vertical lines become horizontal. + elseif checkFuzzy(slope, 0) then + return false -- Horizontal lines become vertical. + else + return -1 / slope + end +end + +-- Gives the y-intercept of a line. +-- x1, y1, x2, y2 +-- x1, y1, slope +local function getYIntercept(x, y, ...) + local input = checkInput(...) + local slope + + if #input == 1 then + slope = input[1] + else + slope = getSlope(x, y, unpack(input)) + end + + if not slope then + return x, true + end -- This way we have some information on the line. + return y - slope * x, false +end + +-- Gives the intersection of two lines. +-- slope1, slope2, x1, y1, x2, y2 +-- slope1, intercept1, slope2, intercept2 +-- x1, y1, x2, y2, x3, y3, x4, y4 +local function getLineLineIntersection(...) + local input = checkInput(...) + local x1, y1, x2, y2, x3, y3, x4, y4 + local slope1, intercept1 + local slope2, intercept2 + local x, y + + if #input == 4 then -- Given slope1, intercept1, slope2, intercept2. + slope1, intercept1, slope2, intercept2 = unpack(input) + + -- Since these are lines, not segments, we can use arbitrary points, such as ( 1, y ), ( 2, y ) + y1 = slope1 and slope1 * 1 + intercept1 or 1 + y2 = slope1 and slope1 * 2 + intercept1 or 2 + y3 = slope2 and slope2 * 1 + intercept2 or 1 + y4 = slope2 and slope2 * 2 + intercept2 or 2 + x1 = slope1 and (y1 - intercept1) / slope1 or intercept1 + x2 = slope1 and (y2 - intercept1) / slope1 or intercept1 + x3 = slope2 and (y3 - intercept2) / slope2 or intercept2 + x4 = slope2 and (y4 - intercept2) / slope2 or intercept2 + elseif #input == 6 then -- Given slope1, intercept1, and 2 points on the other line. + slope1, intercept1 = input[1], input[2] + slope2 = getSlope(input[3], input[4], input[5], input[6]) + intercept2 = getYIntercept(input[3], input[4], input[5], input[6]) + + y1 = slope1 and slope1 * 1 + intercept1 or 1 + y2 = slope1 and slope1 * 2 + intercept1 or 2 + y3 = input[4] + y4 = input[6] + x1 = slope1 and (y1 - intercept1) / slope1 or intercept1 + x2 = slope1 and (y2 - intercept1) / slope1 or intercept1 + x3 = input[3] + x4 = input[5] + elseif #input == 8 then -- Given 2 points on line 1 and 2 points on line 2. + slope1 = getSlope(input[1], input[2], input[3], input[4]) + intercept1 = getYIntercept(input[1], input[2], input[3], input[4]) + slope2 = getSlope(input[5], input[6], input[7], input[8]) + intercept2 = getYIntercept(input[5], input[6], input[7], input[8]) + + x1, y1, x2, y2, x3, y3, x4, y4 = unpack(input) + end + + if not slope1 and not slope2 then -- Both are vertical lines + if x1 == x3 then -- Have to have the same x positions to intersect + return true + else + return false + end + elseif not slope1 then -- First is vertical + x = x1 -- They have to meet at this x, since it is this line's only x + y = slope2 and slope2 * x + intercept2 or 1 + elseif not slope2 then -- Second is vertical + x = x3 -- Vice-Versa + y = slope1 * x + intercept1 + elseif checkFuzzy(slope1, slope2) then -- Parallel (not vertical) + if checkFuzzy(intercept1, intercept2) then -- Same intercept + return true + else + return false + end + else -- Regular lines + x = (-intercept1 + intercept2) / (slope1 - slope2) + y = slope1 * x + intercept1 + end + + return x, y +end + +-- Gives the closest point on a line to a point. +-- perpendicularX, perpendicularY, x1, y1, x2, y2 +-- perpendicularX, perpendicularY, slope, intercept +local function getClosestPoint(perpendicularX, perpendicularY, ...) + local input = checkInput(...) + local x, y, x1, y1, x2, y2, slope, intercept + + if #input == 4 then -- Given perpendicularX, perpendicularY, x1, y1, x2, y2 + x1, y1, x2, y2 = unpack(input) + slope = getSlope(x1, y1, x2, y2) + intercept = getYIntercept(x1, y1, x2, y2) + elseif #input == 2 then -- Given perpendicularX, perpendicularY, slope, intercept + slope, intercept = unpack(input) + x1, y1 = 1, slope and slope * 1 + intercept or 1 -- Need x1 and y1 in case of vertical/horizontal lines. + end + + if not slope then -- Vertical line + x, y = x1, perpendicularY -- Closest point is always perpendicular. + elseif checkFuzzy(slope, 0) then -- Horizontal line + x, y = perpendicularX, y1 + else + local perpendicularSlope = getPerpendicularSlope(slope) + local perpendicularIntercept = getYIntercept(perpendicularX, perpendicularY, perpendicularSlope) + x, y = getLineLineIntersection(slope, intercept, perpendicularSlope, perpendicularIntercept) + end + + return x, y +end + +-- Gives the intersection of a line and a line segment. +-- x1, y1, x2, y2, x3, y3, x4, y4 +-- x1, y1, x2, y2, slope, intercept +local function getLineSegmentIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + + local slope1, intercept1, x, y, lineX1, lineY1, lineX2, lineY2 + local slope2, intercept2 = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + + if #input == 2 then -- Given slope, intercept + slope1, intercept1 = input[1], input[2] + lineX1, lineY1 = 1, slope1 and slope1 + intercept1 + lineX2, lineY2 = 2, slope1 and slope1 * 2 + intercept1 + else -- Given x3, y3, x4, y4 + lineX1, lineY1, lineX2, lineY2 = unpack(input) + slope1 = getSlope(unpack(input)) + intercept1 = getYIntercept(unpack(input)) + end + + if not slope1 and not slope2 then -- Vertical lines + if checkFuzzy(x1, lineX1) then + return x1, y1, x2, y2 + else + return false + end + elseif not slope1 then -- slope1 is vertical + x, y = input[1], slope2 * input[1] + intercept2 + elseif not slope2 then -- slope2 is vertical + x, y = x1, slope1 * x1 + intercept1 + else + x, y = getLineLineIntersection(slope1, intercept1, slope2, intercept2) + end + + local length1, length2, distance + if x == true then -- Lines are collinear. + return x1, y1, x2, y2 + elseif x then -- There is an intersection + length1, length2 = getLength(x1, y1, x, y), getLength(x2, y2, x, y) + distance = getLength(x1, y1, x2, y2) + else -- Lines are parallel but not collinear. + if checkFuzzy(intercept1, intercept2) then + return x1, y1, x2, y2 + else + return false + end + end + + if length1 <= distance and length2 <= distance then + return x, y + else + return false + end +end + +-- Checks if a point is on a line. +-- Does not support the format using slope because vertical lines would be impossible to check. +local function checkLinePoint(x, y, x1, y1, x2, y2) + local m = getSlope(x1, y1, x2, y2) + local b = getYIntercept(x1, y1, m) + + if not m then -- Vertical + return checkFuzzy(x, x1) + end + return checkFuzzy(y, m * x + b) +end -- }}} + +-- Segment -------------------------------------- {{{ +-- Gives the perpendicular bisector of a line. +local function getPerpendicularBisector(x1, y1, x2, y2) + local slope = getSlope(x1, y1, x2, y2) + local midpointX, midpointY = getMidpoint(x1, y1, x2, y2) + return midpointX, midpointY, getPerpendicularSlope(slope) +end + +-- Gives whether or not a point lies on a line segment. +local function checkSegmentPoint(px, py, x1, y1, x2, y2) + -- Explanation around 5:20: https://www.youtube.com/watch?v=A86COO8KC58 + local x = checkLinePoint(px, py, x1, y1, x2, y2) + if not x then + return false + end + + local lengthX = x2 - x1 + local lengthY = y2 - y1 + + if checkFuzzy(lengthX, 0) then -- Vertical line + if checkFuzzy(px, x1) then + local low, high + if y1 > y2 then + low = y2; + high = y1 + else + low = y1; + high = y2 + end + + if py >= low and py <= high then + return true + else + return false + end + else + return false + end + elseif checkFuzzy(lengthY, 0) then -- Horizontal line + if checkFuzzy(py, y1) then + local low, high + if x1 > x2 then + low = x2; + high = x1 + else + low = x1; + high = x2 + end + + if px >= low and px <= high then + return true + else + return false + end + else + return false + end + end + + local distanceToPointX = (px - x1) + local distanceToPointY = (py - y1) + local scaleX = distanceToPointX / lengthX + local scaleY = distanceToPointY / lengthY + + if (scaleX >= 0 and scaleX <= 1) and (scaleY >= 0 and scaleY <= 1) then -- Intersection + return true + end + return false +end + +-- Gives the point of intersection between two line segments. +local function getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4) + local slope1, intercept1 = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + local slope2, intercept2 = getSlope(x3, y3, x4, y4), getYIntercept(x3, y3, x4, y4) + + if ((slope1 and slope2) and checkFuzzy(slope1, slope2)) or (not slope1 and not slope2) then -- Parallel lines + if checkFuzzy(intercept1, intercept2) then -- The same lines, possibly in different points. + local points = {} + if checkSegmentPoint(x1, y1, x3, y3, x4, y4) then + addPoints(points, x1, y1) + end + if checkSegmentPoint(x2, y2, x3, y3, x4, y4) then + addPoints(points, x2, y2) + end + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) then + addPoints(points, x3, y3) + end + if checkSegmentPoint(x4, y4, x1, y1, x2, y2) then + addPoints(points, x4, y4) + end + + points = removeDuplicatePointsFlat(points) + if #points == 0 then + return false + end + return unpack(points) + else + return false + end + end + + local x, y = getLineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) + if x and checkSegmentPoint(x, y, x1, y1, x2, y2) and checkSegmentPoint(x, y, x3, y3, x4, y4) then + return x, y + end + return false +end -- }}} + +-- Math ----------------------------------------- {{{ +-- Get the root of a number (i.e. the 2nd (square) root of 4 is 2) +local function getRoot(number, root) + return number ^ (1 / root) +end + +-- Checks if a number is prime. +local function isPrime(number) + if number < 2 then + return false + end + + for i = 2, math.sqrt(number) do + if number % i == 0 then + return false + end + end + return true +end + +-- Rounds a number to the xth decimal place (round( 3.14159265359, 4 ) --> 3.1416) +local function round(number, place) + local pow = 10 ^ (place or 0) + return math.floor(number * pow + .5) / pow +end + +-- Gives the summation given a local function +local function getSummation(start, stop, func) + local returnValues = {} + local sum = 0 + for i = start, stop do + local value = func(i, returnValues) + returnValues[i] = value + sum = sum + value + end + return sum +end + +-- Gives the percent of change. +local function getPercentOfChange(old, new) + if old == 0 and new == 0 then + return 0 + else + return (new - old) / math.abs(old) + end +end + +-- Gives the percentage of a number. +local function getPercentage(percent, number) + return percent * number +end + +-- Returns the quadratic roots of an equation. +local function getQuadraticRoots(a, b, c) + local discriminant = b ^ 2 - (4 * a * c) + if discriminant < 0 then + return false + end + discriminant = math.sqrt(discriminant) + local denominator = (2 * a) + return (-b - discriminant) / denominator, (-b + discriminant) / denominator +end + +-- Gives the angle between three points. +local function getAngle(x1, y1, x2, y2, x3, y3) + local a = getLength(x3, y3, x2, y2) + local b = getLength(x1, y1, x2, y2) + local c = getLength(x1, y1, x3, y3) + + return math.acos((a * a + b * b - c * c) / (2 * a * b)) +end -- }}} + +-- Circle --------------------------------------- {{{ +-- Gives the area of the circle. +local function getCircleArea(radius) + return math.pi * (radius * radius) +end + +-- Checks if a point is within the radius of a circle. +local function checkCirclePoint(x, y, circleX, circleY, radius) + return getLength(circleX, circleY, x, y) <= radius +end + +-- Checks if a point is on a circle. +local function isPointOnCircle(x, y, circleX, circleY, radius) + return checkFuzzy(getLength(circleX, circleY, x, y), radius) +end + +-- Gives the circumference of a circle. +local function getCircumference(radius) + return 2 * math.pi * radius +end + +-- Gives the intersection of a line and a circle. +local function getCircleLineIntersection(circleX, circleY, radius, x1, y1, x2, y2) + local slope = getSlope(x1, y1, x2, y2) + local intercept = getYIntercept(x1, y1, slope) + + if slope then + local a = (1 + slope ^ 2) + local b = (-2 * (circleX) + (2 * slope * intercept) - (2 * circleY * slope)) + local c = (circleX ^ 2 + intercept ^ 2 - 2 * (circleY) * (intercept) + circleY ^ 2 - radius ^ 2) + + x1, x2 = getQuadraticRoots(a, b, c) + + if not x1 then + return false + end + + y1 = slope * x1 + intercept + y2 = slope * x2 + intercept + + if checkFuzzy(x1, x2) and checkFuzzy(y1, y2) then + return 'tangent', x1, y1 + else + return 'secant', x1, y1, x2, y2 + end + else -- Vertical Lines + local lengthToPoint1 = circleX - x1 + local remainingDistance = lengthToPoint1 - radius + local intercept = math.sqrt(-(lengthToPoint1 ^ 2 - radius ^ 2)) + + if -(lengthToPoint1 ^ 2 - radius ^ 2) < 0 then + return false + end + + local bottomX, bottomY = x1, circleY - intercept + local topX, topY = x1, circleY + intercept + + if topY ~= bottomY then + return 'secant', topX, topY, bottomX, bottomY + else + return 'tangent', topX, topY + end + end +end + +-- Gives the type of intersection of a line segment. +local function getCircleSegmentIntersection(circleX, circleY, radius, x1, y1, x2, y2) + local Type, x3, y3, x4, y4 = getCircleLineIntersection(circleX, circleY, radius, x1, y1, x2, y2) + if not Type then + return false + end + + local slope, intercept = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + + if isPointOnCircle(x1, y1, circleX, circleY, radius) and isPointOnCircle(x2, y2, circleX, circleY, radius) then -- Both points are on line-segment. + return 'chord', x1, y1, x2, y2 + end + + if slope then + if checkCirclePoint(x1, y1, circleX, circleY, radius) and checkCirclePoint(x2, y2, circleX, circleY, radius) then -- Line-segment is fully in circle. + return 'enclosed', x1, y1, x2, y2 + elseif x3 and x4 then + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) and not checkSegmentPoint(x4, y4, x1, y1, x2, y2) then -- Only the first of the points is on the line-segment. + return 'tangent', x3, y3 + elseif checkSegmentPoint(x4, y4, x1, y1, x2, y2) and not checkSegmentPoint(x3, y3, x1, y1, x2, y2) then -- Only the second of the points is on the line-segment. + return 'tangent', x4, y4 + else -- Neither of the points are on the circle (means that the segment is not on the circle, but "encasing" the circle) + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) and checkSegmentPoint(x4, y4, x1, y1, x2, y2) then + return 'secant', x3, y3, x4, y4 + else + return false + end + end + elseif not x4 then -- Is a tangent. + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) then + return 'tangent', x3, y3 + else -- Neither of the points are on the line-segment (means that the segment is not on the circle or "encasing" the circle). + local length = getLength(x1, y1, x2, y2) + local distance1 = getLength(x1, y1, x3, y3) + local distance2 = getLength(x2, y2, x3, y3) + + if length > distance1 or length > distance2 then + return false + elseif length < distance1 and length < distance2 then + return false + else + return 'tangent', x3, y3 + end + end + end + else + local lengthToPoint1 = circleX - x1 + local remainingDistance = lengthToPoint1 - radius + local intercept = math.sqrt(-(lengthToPoint1 ^ 2 - radius ^ 2)) + + if -(lengthToPoint1 ^ 2 - radius ^ 2) < 0 then + return false + end + + local topX, topY = x1, circleY - intercept + local bottomX, bottomY = x1, circleY + intercept + + local length = getLength(x1, y1, x2, y2) + local distance1 = getLength(x1, y1, topX, topY) + local distance2 = getLength(x2, y2, topX, topY) + + if bottomY ~= topY then -- Not a tangent + if checkSegmentPoint(topX, topY, x1, y1, x2, y2) and checkSegmentPoint(bottomX, bottomY, x1, y1, x2, y2) then + return 'chord', topX, topY, bottomX, bottomY + elseif checkSegmentPoint(topX, topY, x1, y1, x2, y2) then + return 'tangent', topX, topY + elseif checkSegmentPoint(bottomX, bottomY, x1, y1, x2, y2) then + return 'tangent', bottomX, bottomY + else + return false + end + else -- Tangent + if checkSegmentPoint(topX, topY, x1, y1, x2, y2) then + return 'tangent', topX, topY + else + return false + end + end + end +end + +-- Checks if one circle intersects another circle. +local function getCircleCircleIntersection(circle1x, circle1y, radius1, circle2x, circle2y, radius2) + local length = getLength(circle1x, circle1y, circle2x, circle2y) + if length > radius1 + radius2 then + return false + end -- If the distance is greater than the two radii, they can't intersect. + if checkFuzzy(length, 0) and checkFuzzy(radius1, radius2) then + return 'equal' + end + if checkFuzzy(circle1x, circle2x) and checkFuzzy(circle1y, circle2y) then + return 'collinear' + end + + local a = (radius1 * radius1 - radius2 * radius2 + length * length) / (2 * length) + local h = math.sqrt(radius1 * radius1 - a * a) + + local p2x = circle1x + a * (circle2x - circle1x) / length + local p2y = circle1y + a * (circle2y - circle1y) / length + local p3x = p2x + h * (circle2y - circle1y) / length + local p3y = p2y - h * (circle2x - circle1x) / length + local p4x = p2x - h * (circle2y - circle1y) / length + local p4y = p2y + h * (circle2x - circle1x) / length + + if not validateNumber(p3x) or not validateNumber(p3y) or not validateNumber(p4x) or not validateNumber(p4y) then + return 'inside' + end + + if checkFuzzy(length, radius1 + radius2) or checkFuzzy(length, math.abs(radius1 - radius2)) then + return 'tangent', p3x, p3y + end + return 'intersection', p3x, p3y, p4x, p4y +end + +-- Checks if circle1 is entirely inside of circle2. +local function isCircleCompletelyInsideCircle(circle1x, circle1y, circle1radius, circle2x, circle2y, circle2radius) + if not checkCirclePoint(circle1x, circle1y, circle2x, circle2y, circle2radius) then + return false + end + local Type = getCircleCircleIntersection(circle2x, circle2y, circle2radius, circle1x, circle1y, circle1radius) + if (Type ~= 'tangent' and Type ~= 'collinear' and Type ~= 'inside') then + return false + end + return true +end + +-- Checks if a line-segment is entirely within a circle. +local function isSegmentCompletelyInsideCircle(circleX, circleY, circleRadius, x1, y1, x2, y2) + local Type = getCircleSegmentIntersection(circleX, circleY, circleRadius, x1, y1, x2, y2) + return Type == 'enclosed' +end -- }}} + +-- Polygon -------------------------------------- {{{ +-- Gives the signed area. +-- If the points are clockwise the number is negative, otherwise, it's positive. +local function getSignedPolygonArea(...) + local points = checkInput(...) + + -- Shoelace formula (https://en.wikipedia.org/wiki/Shoelace_formula). + points[#points + 1] = points[1] + points[#points + 1] = points[2] + + return (.5 * getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1])) + end)) +end + +-- Simply returns the area of the polygon. +local function getPolygonArea(...) + return math.abs(getSignedPolygonArea(...)) +end + +-- Gives the height of a triangle, given the base. +-- base, x1, y1, x2, y2, x3, y3, x4, y4 +-- base, area +local function getTriangleHeight(base, ...) + local input = checkInput(...) + local area + + if #input == 1 then + area = input[1] -- Given area. + else + area = getPolygonArea(input) + end -- Given coordinates. + + return (2 * area) / base, area +end + +-- Gives the centroid of the polygon. +local function getCentroid(...) + local points = checkInput(...) + + points[#points + 1] = points[1] + points[#points + 1] = points[2] + + local area = getSignedPolygonArea(points) -- Needs to be signed here in case points are counter-clockwise. + + -- This formula: https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon + local centroidX = (1 / (6 * area)) * (getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ((points[index] + cycle(points, index + 2)) * + ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1]))) + end)) + + local centroidY = (1 / (6 * area)) * (getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ((points[index + 1] + cycle(points, index + 3)) * + ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1]))) + end)) + + return centroidX, centroidY +end + +-- Returns whether or not a line intersects a polygon. +-- x1, y1, x2, y2, polygonPoints +local function getPolygonLineIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + local choices = {} + + local slope = getSlope(x1, y1, x2, y2) + local intercept = getYIntercept(x1, y1, slope) + + local x3, y3, x4, y4 + if slope then + x3, x4 = 1, 2 + y3, y4 = slope * x3 + intercept, slope * x4 + intercept + else + x3, x4 = x1, x1 + y3, y4 = y1, y2 + end + + for i = 1, #input, 2 do + local x1, y1, x2, y2 = getLineSegmentIntersection(input[i], input[i + 1], cycle(input, i + 2), + cycle(input, i + 3), x3, y3, x4, y4) + if x1 and not x2 then + choices[#choices + 1] = {x1, y1} + elseif x1 and x2 then + choices[#choices + 1] = {x1, y1, x2, y2} + end + -- No need to check 2-point sets since they only intersect each poly line once. + end + + local final = removeDuplicatePairs(choices) + return #final > 0 and final or false +end + +-- Returns if the line segment intersects the polygon. +-- x1, y1, x2, y2, polygonPoints +local function getPolygonSegmentIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + local choices = {} + + for i = 1, #input, 2 do + local x1, y1, x2, y2 = getSegmentSegmentIntersection(input[i], input[i + 1], cycle(input, i + 2), + cycle(input, i + 3), x1, y1, x2, y2) + if x1 and not x2 then + choices[#choices + 1] = {x1, y1} + elseif x2 then + choices[#choices + 1] = {x1, y1, x2, y2} + end + end + + local final = removeDuplicatePairs(choices) + return #final > 0 and final or false +end + +-- Checks if the point lies INSIDE the polygon not on the polygon. +local function checkPolygonPoint(px, py, ...) + local points = {unpack(checkInput(...))} -- Make a new table, as to not edit values of previous. + + local greatest, least = getGreatestPoint(points, 0) + if not isWithinBounds(least, py, greatest) then + return false + end + greatest, least = getGreatestPoint(points) + if not isWithinBounds(least, px, greatest) then + return false + end + + local count = 0 + for i = 1, #points, 2 do + if checkFuzzy(points[i + 1], py) then + points[i + 1] = py + .001 -- Handles vertices that lie on the point. + -- Not exactly mathematically correct, but a lot easier. + end + if points[i + 3] and checkFuzzy(points[i + 3], py) then + points[i + 3] = py + .001 -- Do not need to worry about alternate case, since points[2] has already been done. + end + local x1, y1 = points[i], points[i + 1] + local x2, y2 = points[i + 2] or points[1], points[i + 3] or points[2] + + if getSegmentSegmentIntersection(px, py, greatest, py, x1, y1, x2, y2) then + count = count + 1 + end + end + + return count and count % 2 ~= 0 +end + +-- Returns if the line segment is fully or partially inside. +-- x1, y1, x2, y2, polygonPoints +local function isSegmentInsidePolygon(x1, y1, x2, y2, ...) + local input = checkInput(...) + + local choices = getPolygonSegmentIntersection(x1, y1, x2, y2, input) -- If it's partially enclosed that's all we need. + if choices then + return true + end + + if checkPolygonPoint(x1, y1, input) or checkPolygonPoint(x2, y2, input) then + return true + end + return false +end + +-- Returns whether two polygons intersect. +local function getPolygonPolygonIntersection(polygon1, polygon2) + local choices = {} + + for index1 = 1, #polygon1, 2 do + local intersections = getPolygonSegmentIntersection(polygon1[index1], polygon1[index1 + 1], + cycle(polygon1, index1 + 2), cycle(polygon1, index1 + 3), polygon2) + if intersections then + for index2 = 1, #intersections do + choices[#choices + 1] = intersections[index2] + end + end + end + + for index1 = 1, #polygon2, 2 do + local intersections = getPolygonSegmentIntersection(polygon2[index1], polygon2[index1 + 1], + cycle(polygon2, index1 + 2), cycle(polygon2, index1 + 3), polygon1) + if intersections then + for index2 = 1, #intersections do + choices[#choices + 1] = intersections[index2] + end + end + end + + choices = removeDuplicatePairs(choices) + for i = #choices, 1, -1 do + if type(choices[i][1]) == 'table' then -- Remove co-linear pairs. + table.remove(choices, i) + end + end + + return #choices > 0 and choices +end + +-- Returns whether the circle intersects the polygon. +-- x, y, radius, polygonPoints +local function getPolygonCircleIntersection(x, y, radius, ...) + local input = checkInput(...) + local choices = {} + + for i = 1, #input, 2 do + local Type, x1, y1, x2, y2 = getCircleSegmentIntersection(x, y, radius, input[i], input[i + 1], + cycle(input, i + 2), cycle(input, i + 3)) + if x2 then + choices[#choices + 1] = {Type, x1, y1, x2, y2} + elseif x1 then + choices[#choices + 1] = {Type, x1, y1} + end + end + + local final = removeDuplicates4Points(choices) + + return #final > 0 and final +end + +-- Returns whether the circle is inside the polygon. +-- x, y, radius, polygonPoints +local function isCircleInsidePolygon(x, y, radius, ...) + local input = checkInput(...) + return checkPolygonPoint(x, y, input) +end + +-- Returns whether the polygon is inside the polygon. +local function isPolygonInsidePolygon(polygon1, polygon2) + local bool = false + for i = 1, #polygon2, 2 do + local result = false + result = isSegmentInsidePolygon(polygon2[i], polygon2[i + 1], cycle(polygon2, i + 2), cycle(polygon2, i + 3), + polygon1) + if result then + bool = true; + break + end + end + return bool +end + +-- Checks if a segment is completely inside a polygon +local function isSegmentCompletelyInsidePolygon(x1, y1, x2, y2, ...) + local polygon = checkInput(...) + if not checkPolygonPoint(x1, y1, polygon) or not checkPolygonPoint(x2, y2, polygon) or + getPolygonSegmentIntersection(x1, y1, x2, y2, polygon) then + return false + end + return true +end + +-- Checks if a polygon is completely inside another polygon +local function isPolygonCompletelyInsidePolygon(polygon1, polygon2) + for i = 1, #polygon1, 2 do + local x1, y1 = polygon1[i], polygon1[i + 1] + local x2, y2 = polygon1[i + 2] or polygon1[1], polygon1[i + 3] or polygon1[2] + if not isSegmentCompletelyInsidePolygon(x1, y1, x2, y2, polygon2) then + return false + end + end + return true +end + +-------------- Circle w/ Polygons -------------- +-- Gets if a polygon is completely within a circle +-- circleX, circleY, circleRadius, polygonPoints +local function isPolygonCompletelyInsideCircle(circleX, circleY, circleRadius, ...) + local input = checkInput(...) + local function isDistanceLess(px, py, x, y, circleRadius) -- Faster, does not use math.sqrt + local distanceX, distanceY = px - x, py - y + return distanceX * distanceX + distanceY * distanceY < circleRadius * circleRadius -- Faster. For comparing distances only. + end + + for i = 1, #input, 2 do + if not checkCirclePoint(input[i], input[i + 1], circleX, circleY, circleRadius) then + return false + end + end + return true +end + +-- Checks if a circle is completely within a polygon +-- circleX, circleY, circleRadius, polygonPoints +local function isCircleCompletelyInsidePolygon(circleX, circleY, circleRadius, ...) + local input = checkInput(...) + if not checkPolygonPoint(circleX, circleY, ...) then + return false + end + + local rad2 = circleRadius * circleRadius + + for i = 1, #input, 2 do + local x1, y1 = input[i], input[i + 1] + local x2, y2 = input[i + 2] or input[1], input[i + 3] or input[2] + if distance2(x1, y1, circleX, circleY) <= rad2 then + return false + end + if getCircleSegmentIntersection(circleX, circleY, circleRadius, x1, y1, x2, y2) then + return false + end + end + return true +end -- }}} + +-- Statistics ----------------------------------- {{{ +-- Gets the average of a list of points +-- points +local function getMean(...) + local input = checkInput(...) + + local mean = getSummation(1, #input, function(i, t) + return input[i] + end) / #input + + return mean +end + +local function getMedian(...) + local input = checkInput(...) + + table.sort(input) + + local median + if #input % 2 == 0 then -- If you have an even number of terms, you need to get the average of the middle 2. + median = getMean(input[#input / 2], input[#input / 2 + 1]) + else + median = input[#input / 2 + .5] + end + + return median +end + +-- Gets the mode of a number. +local function getMode(...) + local input = checkInput(...) + + table.sort(input) + local sorted = {} + for i = 1, #input do + local value = input[i] + sorted[value] = sorted[value] and sorted[value] + 1 or 1 + end + + local occurrences, least = 0, {} + for i, value in pairs(sorted) do + if value > occurrences then + least = {i} + occurrences = value + elseif value == occurrences then + least[#least + 1] = i + end + end + + if #least >= 1 then + return least, occurrences + else + return false + end +end + +-- Gets the range of the numbers. +local function getRange(...) + local input = checkInput(...) + local high, low = math.max(unpack(input)), math.min(unpack(input)) + return high - low +end + +-- Gets the variance of a set of numbers. +local function getVariance(...) + local input = checkInput(...) + local mean = getMean(...) + local sum = 0 + for i = 1, #input do + sum = sum + (mean - input[i]) * (mean - input[i]) + end + return sum / #input +end + +-- Gets the standard deviation of a set of numbers. +local function getStandardDeviation(...) + return math.sqrt(getVariance(...)) +end + +-- Gets the central tendency of a set of numbers. +local function getCentralTendency(...) + local mode, occurrences = getMode(...) + return mode, occurrences, getMedian(...), getMean(...) +end + +-- Gets the variation ratio of a data set. +local function getVariationRatio(...) + local input = checkInput(...) + local numbers, times = getMode(...) + times = times * #numbers -- Account for bimodal data + return 1 - (times / #input) +end + +-- Gets the measures of dispersion of a data set. +local function getDispersion(...) + return getVariationRatio(...), getRange(...), getStandardDeviation(...) +end -- }}} + +-- Vector 2 ------------------------------------- {{{ +--[[ + Vector2 Copyright (c) 2010-2013 Matthias Richter + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + Except as contained in this notice, the name(s) of the above copyright holders + shall not be used in advertising or otherwise to promote the sale, use or + other dealings in this Software without prior written authorization. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ]] -- + +local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2 + +local function newVector(x, y) + return { + x = x or 0, + y = y or 0, + } +end + +local function isVector(a) + return type(a.x) == "number" and type(a.y) == "number" +end + +local function cloneVector(a) + return newVector(a.x, a.y) +end + +local function unpackVector(a) + return a.x, a.y +end + +local function toStringVector(a) + return string.format("(%f,%f)", a.x, a.y) +end + +local function invertVector(a) + return newVector(-a.x, -a.y) +end + +local function addVector(a, b) + if type(a) == "table" and type(b) == "table" then + return newVector(a.x + b.x, a.y + b.y) + elseif type(a) == "table" and type(b) == "number" then + return newVector(a.x + b, a.y + b) + elseif type(a) == "number" and type(b) == "table" then + return newVector(a + b.x, a + b.y) + end +end + +local function subVector(a, b) + if type(a) == "table" and type(b) == "table" then + return newVector(a.x - b.x, a.y - b.y) + elseif type(a) == "table" and type(b) == "number" then + return newVector(a.x - b, a.y - b) + elseif type(a) == "number" and type(b) == "table" then + return newVector(a - b.x, a - b.y) + end +end + +local function mulVector(a, b) + if type(a) == "table" and type(b) == "table" then + return newVector(a.x * b.x, a.y * b.y) + elseif type(a) == "table" and type(b) == "number" then + return newVector(a.x * b, a.y * b) + elseif type(a) == "number" and type(b) == "table" then + return newVector(a * b.x, a * b.y) + end +end + +local function divVector(a, b) + if type(a) == "table" and type(b) == "table" then + return newVector(a.x / b.x, a.y / b.y) + elseif type(a) == "table" and type(b) == "number" then + return newVector(a.x / b, a.y / b) + elseif type(a) == "number" and type(b) == "table" then + return newVector(a / b.x, a / b.y) + end +end + +local function eqVector(a, b) + return a.x == b.x and a.y == b.y +end + +local function ltVector(a, b) + return a.x < b.x or (a.x == b.x and a.y < b.y) +end + +local function leVector(a, b) + return a.x <= b.x and a.y <= b.y +end + +local function gtVector(a, b) + return ltVector(b, a) +end + +local function geVector(a, b) + return leVector(b, a) +end + +local function dotVector(a, b) + return a.x * b.x + a.y * b.y +end + +local function len2Vector(a) + return a.x * a.x + a.y * a.y +end + +local function lenVector(a) + return sqrt(len2Vector(a)) +end + +local function dist2Vector(a, b) + local dx = a.x - b.x + local dy = a.y - b.y + return (dx * dx + dy * dy) +end + +local function distVector(a, b) + return sqrt(dist2Vector(a, b)) +end + +local function normalizeVector(a) + local l = lenVector(a) + + if l > 0 then + return newVector(a.x / l, a.y / l) + else + return newVector(a.x, a.y) + end +end + +local function rotateVector(a, phi) + local c, s = cos(phi), sin(phi) + return newVector(c * a.x - s * a.y, s * a.x + c * a.y) +end + +local function perpendicularVector(a) + return newVector(-a.y, a.x) +end + +local function projectOnVector(a, b) + local s = (a.x * b.x + a.y * b.y) / (b.x * b.x + b.y * b.y) + return newVector(s * b.x, s * b.y) +end + +local function mirrorOnVector(a, b) + local s = 2 * (a.x * b.x + a.y * b.y) / (b.x * b.x + b.y * b.y) + return newVector(s * b.x - a.x, s * b.y - a.y) +end + +local function crossVector(a, b) + return a.x * b.y - a.y * b.x +end + +-- ref.: http://blog.signalsondisplay.com/?p=336 +local function trimVector(a, maxLen) + local s = maxLen * maxLen / len2Vector(a) + s = (s > 1 and 1) or sqrt(s) + return newVector(a.x * s, a.y * s) +end + +local function angleToVector(a, b) + if b then + return atan2(a.y - b.y, a.x - b.x) + end + + return atan2(a.y, a.x) +end + +local function lerpVector(a, b, s) + return a + s * (b - a) +end -- }}} + +return { + _VERSION = 'MLib 0.11.0', + _DESCRIPTION = 'A math and shape-intersection detection library for Lua', + _URL = 'https://github.com/davisdude/mlib', + point = { + rotate = rotatePoint, + scale = scalePoint, + polarToCartesian = polarToCartesian, + cartesianToPolar = cartesianToPolar, + }, + line = { + getLength = getLength, + getMidpoint = getMidpoint, + getSlope = getSlope, + getPerpendicularSlope = getPerpendicularSlope, + getYIntercept = getYIntercept, + getIntersection = getLineLineIntersection, + getClosestPoint = getClosestPoint, + getSegmentIntersection = getLineSegmentIntersection, + checkPoint = checkLinePoint, + + -- Aliases + getDistance = getLength, + getCircleIntersection = getCircleLineIntersection, + getPolygonIntersection = getPolygonLineIntersection, + getLineIntersection = getLineLineIntersection, + }, + segment = { + checkPoint = checkSegmentPoint, + getPerpendicularBisector = getPerpendicularBisector, + getIntersection = getSegmentSegmentIntersection, + + -- Aliases + getCircleIntersection = getCircleSegmentIntersection, + getPolygonIntersection = getPolygonSegmentIntersection, + getLineIntersection = getLineSegmentIntersection, + getSegmentIntersection = getSegmentSegmentIntersection, + isSegmentCompletelyInsideCircle = isSegmentCompletelyInsideCircle, + isSegmentCompletelyInsidePolygon = isSegmentCompletelyInsidePolygon, + }, + math = { + getRoot = getRoot, + isPrime = isPrime, + round = round, + getSummation = getSummation, + getPercentOfChange = getPercentOfChange, + getPercentage = getPercentage, + getQuadraticRoots = getQuadraticRoots, + getAngle = getAngle, + }, + circle = { + getArea = getCircleArea, + checkPoint = checkCirclePoint, + isPointOnCircle = isPointOnCircle, + getCircumference = getCircumference, + getLineIntersection = getCircleLineIntersection, + getSegmentIntersection = getCircleSegmentIntersection, + getCircleIntersection = getCircleCircleIntersection, + isCircleCompletelyInside = isCircleCompletelyInsideCircle, + isPolygonCompletelyInside = isPolygonCompletelyInsideCircle, + isSegmentCompletelyInside = isSegmentCompletelyInsideCircle, + + -- Aliases + getPolygonIntersection = getPolygonCircleIntersection, + isCircleInsidePolygon = isCircleInsidePolygon, + isCircleCompletelyInsidePolygon = isCircleCompletelyInsidePolygon, + }, + polygon = { + getSignedArea = getSignedPolygonArea, + getArea = getPolygonArea, + getTriangleHeight = getTriangleHeight, + getCentroid = getCentroid, + getLineIntersection = getPolygonLineIntersection, + getSegmentIntersection = getPolygonSegmentIntersection, + checkPoint = checkPolygonPoint, + isSegmentInside = isSegmentInsidePolygon, + getPolygonIntersection = getPolygonPolygonIntersection, + getCircleIntersection = getPolygonCircleIntersection, + isCircleInside = isCircleInsidePolygon, + isPolygonInside = isPolygonInsidePolygon, + isCircleCompletelyInside = isCircleCompletelyInsidePolygon, + isSegmentCompletelyInside = isSegmentCompletelyInsidePolygon, + isPolygonCompletelyInside = isPolygonCompletelyInsidePolygon, + + -- Aliases + isCircleCompletelyOver = isPolygonCompletelyInsideCircle, + }, + statistics = { + getMean = getMean, + getMedian = getMedian, + getMode = getMode, + getRange = getRange, + getVariance = getVariance, + getStandardDeviation = getStandardDeviation, + getCentralTendency = getCentralTendency, + getVariationRatio = getVariationRatio, + getDispersion = getDispersion, + }, + vec2 = { + new = newVector, + isVector = isVector, + clone = cloneVector, + toString = toStringVector, + invert = invertVector, + add = addVector, + sub = subVector, + mul = mulVector, + div = divVector, + eq = eqVector, + lt = ltVector, + le = leVector, + gt = gtVector, + ge = geVector, + dot = dotVector, + len = lenVector, + len2 = len2Vector, + dist = distVector, + dist2 = dist2Vector, + normalize = normalizeVector, + rotate = rotateVector, + perpendicular = perpendicularVector, + projectOn = projectOnVector, + mirrorOn = mirrorOnVector, + cross = crossVector, + trim = trimVector, + angleTo = angleToVector, + lerp = lerpVector, + + -- Aliases + copy = cloneVector, + subtract = subVector, + multiply = mulVector, + divide = divVector, + equal = eqVector, + lessThan = ltVector, + lessThanOrEqualTo = leVector, + greaterThan = gtVector, + greaterThanOrEqualTo = geVector, + dotProduct = dotVector, + length = lenVector, + length2 = len2Vector, + distance = distVector, + distance2 = dist2Vector, + }, +} diff --git a/framework/lualib/3rd/misc/switch.lua b/framework/lualib/3rd/misc/switch.lua new file mode 100644 index 0000000..6e594d1 --- /dev/null +++ b/framework/lualib/3rd/misc/switch.lua @@ -0,0 +1,41 @@ +local switch = {} + +switch.__index = switch + +switch.__call = function(self, v) + local c = self._callbacks[v] or self._default + assert(c, "No case statement defined for variable, and :default is not defined") + c() +end + +function switch:case(v, f) + self._callbacks[v] = f + return self +end + +function switch:default(f) + self._default = f + return self +end + +return function() + return setmetatable({ + _callbacks = {}, + }, switch) +end + +-- local switch = require 'switch' + +-- local numbercase = switch() +-- :case(10, function() +-- print("Hello Ten") +-- end) + +-- :case(20, function() +-- print("Hello Twenty") +-- end) + +-- :default(function() +-- print("Unrecognised") +-- end) +-- numbercase(15 + 5) diff --git a/framework/lualib/3rd/misc/tuple.lua b/framework/lualib/3rd/misc/tuple.lua new file mode 100644 index 0000000..bb2e5d9 --- /dev/null +++ b/framework/lualib/3rd/misc/tuple.lua @@ -0,0 +1,148 @@ +-- ========================================= +-- tuple, A minimal tuple class for Lua +-- https://github.com/Yonaba/tuple.lua +-- ========================================= +local unpack = unpack or table.unpack +local setmetatable = setmetatable +local ipairs = ipairs +local tostring = tostring +local min = math.min +local type = type +local assert = assert +local select = select +local t_concat = table.concat + +local tuple = {} +tuple.__index = tuple + +-- Collects values i to j to return a new tuple +function tuple.__call(t, i, j) + return tuple(unpack(t, i, j)) +end + +-- Returns a string representation of tuple +function tuple:__tostring() + local t = self:toArray() + for k, v in ipairs(t) do + t[k] = tostring(v) + end + return ('(%s)'):format(t_concat(t, ', ')) +end + +-- Tuple elements iterator function +function tuple:elements(...) + local i = 0 + return function(...) + i = i + 1 + if self[i] ~= nil then + return i, self[i] + end + end +end + +-- Returns tuple length +function tuple:len() + return self.n +end + +-- Tests if tuple contains element v +function tuple:has(v) + for k, _v in ipairs(self) do + if _v == v then + return true + end + end + return false +end + +-- Does this tuple includes all elements in tuple other ? +function tuple:includes(other) + if self.n < other.n then + return false + end + for _, element in other:elements() do + if not self:has(element) then + return false + end + end + return true +end + +-- Converts tuple to simpe array ? +function tuple:toArray() + return {unpack(self)} +end + +-- ========== +-- Operators +-- ========== + +-- Tuple equality test +function tuple:__eq(other) + if self.n ~= other.n then + return false + end + for i, element in other:elements() do + if element ~= self[i] then + return false + end + end + return true +end + +-- Tuple relational <= comparison +function tuple:__le(other) + local n = min(self.n, other.n) + for i = 1, n do + if (self[i] > other[i]) then + return false + end + end + return true +end + +-- Tuple relational < comparison +function tuple:__lt(other) + local n = min(self.n, other.n) + for i = 1, n do + if self[i] >= other[i] then + return false + end + end + return true +end + +-- Tuple addition +function tuple.__add(a, b) + local t = a() + for _, element in b:elements() do + t[#t + 1] = element + end + t.n = #t + return t +end + +-- Multiplication +function tuple.__mul(t, n) + if type(n) == 'number' then + assert(math.floor(n) == n, ('Wrong argument n. Integer expected, got (%s)'):format(n)) + local _t + for i = 1, n do + _t = (_t or tuple()) + t + end + return _t + else + return n * t + end +end + +-- Class constructor, wrapping up and return +return setmetatable(tuple, { + __call = function(self, ...) + local new_tuple = { + n = select('#', ...), + ..., + } + return setmetatable(new_tuple, tuple) + end, +})