diff --git a/.gitignore b/.gitignore index aa34b5aaf7..704714139f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ src/Data/TimelessJewelData/*.bin # Simplegraphic Debugging runtime/imgui.ini runtime/SimpleGraphic/SimpleGraphic.log + +src/poe_api_response.json \ No newline at end of file diff --git a/runtime/lua/sha2.lua b/runtime/lua/sha2.lua new file mode 100644 index 0000000000..201f52ea67 --- /dev/null +++ b/runtime/lua/sha2.lua @@ -0,0 +1,5675 @@ +-------------------------------------------------------------------------------------------------------------------------- +-- sha2.lua +-------------------------------------------------------------------------------------------------------------------------- +-- VERSION: 12 (2022-02-23) +-- AUTHOR: Egor Skriptunoff +-- LICENSE: MIT (the same license as Lua itself) +-- URL: https://github.com/Egor-Skriptunoff/pure_lua_SHA +-- +-- DESCRIPTION: +-- This module contains functions to calculate SHA digest: +-- MD5, SHA-1, +-- SHA-224, SHA-256, SHA-512/224, SHA-512/256, SHA-384, SHA-512, +-- SHA3-224, SHA3-256, SHA3-384, SHA3-512, SHAKE128, SHAKE256, +-- HMAC, +-- BLAKE2b, BLAKE2s, BLAKE2bp, BLAKE2sp, BLAKE2Xb, BLAKE2Xs, +-- BLAKE3, BLAKE3_KDF +-- Written in pure Lua. +-- Compatible with: +-- Lua 5.1, Lua 5.2, Lua 5.3, Lua 5.4, Fengari, LuaJIT 2.0/2.1 (any CPU endianness). +-- Main feature of this module: it was heavily optimized for speed. +-- For every Lua version the module contains particular implementation branch to get benefits from version-specific features. +-- - branch for Lua 5.1 (emulating bitwise operators using look-up table) +-- - branch for Lua 5.2 (using bit32/bit library), suitable for both Lua 5.2 with native "bit32" and Lua 5.1 with external library "bit" +-- - branch for Lua 5.3/5.4 (using native 64-bit bitwise operators) +-- - branch for Lua 5.3/5.4 (using native 32-bit bitwise operators) for Lua built with LUA_INT_TYPE=LUA_INT_INT +-- - branch for LuaJIT without FFI library (useful in a sandboxed environment) +-- - branch for LuaJIT x86 without FFI library (LuaJIT x86 has oddity because of lack of CPU registers) +-- - branch for LuaJIT 2.0 with FFI library (bit.* functions work only with Lua numbers) +-- - branch for LuaJIT 2.1 with FFI library (bit.* functions can work with "int64_t" arguments) +-- +-- +-- USAGE: +-- Input data should be provided as a binary string: either as a whole string or as a sequence of substrings (chunk-by-chunk loading, total length < 9*10^15 bytes). +-- Result (SHA digest) is returned in hexadecimal representation as a string of lowercase hex digits. +-- Simplest usage example: +-- local sha = require("sha2") +-- local your_hash = sha.sha256("your string") +-- See file "sha2_test.lua" for more examples. +-- +-- +-- CHANGELOG: +-- version date description +-- ------- ---------- ----------- +-- 12 2022-02-23 Now works in Luau (but NOT optimized for speed) +-- 11 2022-01-09 BLAKE3 added +-- 10 2022-01-02 BLAKE2 functions added +-- 9 2020-05-10 Now works in OpenWrt's Lua (dialect of Lua 5.1 with "double" + "invisible int32") +-- 8 2019-09-03 SHA-3 functions added +-- 7 2019-03-17 Added functions to convert to/from base64 +-- 6 2018-11-12 HMAC added +-- 5 2018-11-10 SHA-1 added +-- 4 2018-11-03 MD5 added +-- 3 2018-11-02 Bug fixed: incorrect hashing of long (2 GByte) data streams on Lua 5.3/5.4 built with "int32" integers +-- 2 2018-10-07 Decreased module loading time in Lua 5.1 implementation branch (thanks to Peter Melnichenko for giving a hint) +-- 1 2018-10-06 First release (only SHA-2 functions) +----------------------------------------------------------------------------- + + +local print_debug_messages = false -- set to true to view some messages about your system's abilities and implementation branch chosen for your system + +local unpack, table_concat, byte, char, string_rep, sub, gsub, gmatch, string_format, floor, ceil, math_min, math_max, tonumber, type, math_huge = + table.unpack or unpack, table.concat, string.byte, string.char, string.rep, string.sub, string.gsub, string.gmatch, string.format, math.floor, math.ceil, math.min, math.max, tonumber, type, math.huge + + +-------------------------------------------------------------------------------- +-- EXAMINING YOUR SYSTEM +-------------------------------------------------------------------------------- + +local function get_precision(one) + -- "one" must be either float 1.0 or integer 1 + -- returns bits_precision, is_integer + -- This function works correctly with all floating point datatypes (including non-IEEE-754) + local k, n, m, prev_n = 0, one, one + while true do + k, prev_n, n, m = k + 1, n, n + n + 1, m + m + k % 2 + if k > 256 or n - (n - 1) ~= 1 or m - (m - 1) ~= 1 or n == m then + return k, false -- floating point datatype + elseif n == prev_n then + return k, true -- integer datatype + end + end +end + +-- Make sure Lua has "double" numbers +local x = 2/3 +local Lua_has_double = x * 5 > 3 and x * 4 < 3 and get_precision(1.0) >= 53 +assert(Lua_has_double, "at least 53-bit floating point numbers are required") + +-- Q: +-- SHA2 was designed for FPU-less machines. +-- So, why floating point numbers are needed for this module? +-- A: +-- 53-bit "double" numbers are useful to calculate "magic numbers" used in SHA. +-- I prefer to write 50 LOC "magic numbers calculator" instead of storing more than 200 constants explicitly in this source file. + +local int_prec, Lua_has_integers = get_precision(1) +local Lua_has_int64 = Lua_has_integers and int_prec == 64 +local Lua_has_int32 = Lua_has_integers and int_prec == 32 +assert(Lua_has_int64 or Lua_has_int32 or not Lua_has_integers, "Lua integers must be either 32-bit or 64-bit") + +-- Q: +-- Does it mean that almost all non-standard configurations are not supported? +-- A: +-- Yes. Sorry, too many problems to support all possible Lua numbers configurations. +-- Lua 5.1/5.2 with "int32" will not work. +-- Lua 5.1/5.2 with "int64" will not work. +-- Lua 5.1/5.2 with "int128" will not work. +-- Lua 5.1/5.2 with "float" will not work. +-- Lua 5.1/5.2 with "double" is OK. (default config for Lua 5.1, Lua 5.2, LuaJIT) +-- Lua 5.3/5.4 with "int32" + "float" will not work. +-- Lua 5.3/5.4 with "int64" + "float" will not work. +-- Lua 5.3/5.4 with "int128" + "float" will not work. +-- Lua 5.3/5.4 with "int32" + "double" is OK. (config used by Fengari) +-- Lua 5.3/5.4 with "int64" + "double" is OK. (default config for Lua 5.3, Lua 5.4) +-- Lua 5.3/5.4 with "int128" + "double" will not work. +-- Using floating point numbers better than "double" instead of "double" is OK (non-IEEE-754 floating point implementation are allowed). +-- Using "int128" instead of "int64" is not OK: "int128" would require different branch of implementation for optimized SHA512. + +-- Check for LuaJIT and 32-bit bitwise libraries +local is_LuaJIT = ({false, [1] = true})[1] and _VERSION ~= "Luau" and (type(jit) ~= "table" or jit.version_num >= 20000) -- LuaJIT 1.x.x and Luau are treated as vanilla Lua 5.1/5.2 +local is_LuaJIT_21 -- LuaJIT 2.1+ +local LuaJIT_arch +local ffi -- LuaJIT FFI library (as a table) +local b -- 32-bit bitwise library (as a table) +local library_name + +if is_LuaJIT then + -- Assuming "bit" library is always available on LuaJIT + b = require"bit" + library_name = "bit" + -- "ffi" is intentionally disabled on some systems for safety reason + local LuaJIT_has_FFI, result = pcall(require, "ffi") + if LuaJIT_has_FFI then + ffi = result + end + is_LuaJIT_21 = not not loadstring"b=0b0" + LuaJIT_arch = type(jit) == "table" and jit.arch or ffi and ffi.arch or nil +else + -- For vanilla Lua, "bit"/"bit32" libraries are searched in global namespace only. No attempt is made to load a library if it's not loaded yet. + for _, libname in ipairs(_VERSION == "Lua 5.2" and {"bit32", "bit"} or {"bit", "bit32"}) do + if type(_G[libname]) == "table" and _G[libname].bxor then + b = _G[libname] + library_name = libname + break + end + end +end + +-------------------------------------------------------------------------------- +-- You can disable here some of your system's abilities (for testing purposes) +-------------------------------------------------------------------------------- +-- is_LuaJIT = nil +-- is_LuaJIT_21 = nil +-- ffi = nil +-- Lua_has_int32 = nil +-- Lua_has_int64 = nil +-- b, library_name = nil +-------------------------------------------------------------------------------- + +if print_debug_messages then + -- Printing list of abilities of your system + print("Abilities:") + print(" Lua version: "..(is_LuaJIT and "LuaJIT "..(is_LuaJIT_21 and "2.1 " or "2.0 ")..(LuaJIT_arch or "")..(ffi and " with FFI" or " without FFI") or _VERSION)) + print(" Integer bitwise operators: "..(Lua_has_int64 and "int64" or Lua_has_int32 and "int32" or "no")) + print(" 32-bit bitwise library: "..(library_name or "not found")) +end + +-- Selecting the most suitable implementation for given set of abilities +local method, branch +if is_LuaJIT and ffi then + method = "Using 'ffi' library of LuaJIT" + branch = "FFI" +elseif is_LuaJIT then + method = "Using special code for sandboxed LuaJIT (no FFI)" + branch = "LJ" +elseif Lua_has_int64 then + method = "Using native int64 bitwise operators" + branch = "INT64" +elseif Lua_has_int32 then + method = "Using native int32 bitwise operators" + branch = "INT32" +elseif library_name then -- when bitwise library is available (Lua 5.2 with native library "bit32" or Lua 5.1 with external library "bit") + method = "Using '"..library_name.."' library" + branch = "LIB32" +else + method = "Emulating bitwise operators using look-up table" + branch = "EMUL" +end + +if print_debug_messages then + -- Printing the implementation selected to be used on your system + print("Implementation selected:") + print(" "..method) +end + + +-------------------------------------------------------------------------------- +-- BASIC 32-BIT BITWISE FUNCTIONS +-------------------------------------------------------------------------------- + +local AND, OR, XOR, SHL, SHR, ROL, ROR, NOT, NORM, HEX, XOR_BYTE +-- Only low 32 bits of function arguments matter, high bits are ignored +-- The result of all functions (except HEX) is an integer inside "correct range": +-- for "bit" library: (-2^31)..(2^31-1) +-- for "bit32" library: 0..(2^32-1) + +if branch == "FFI" or branch == "LJ" or branch == "LIB32" then + + -- Your system has 32-bit bitwise library (either "bit" or "bit32") + + AND = b.band -- 2 arguments + OR = b.bor -- 2 arguments + XOR = b.bxor -- 2..5 arguments + SHL = b.lshift -- second argument is integer 0..31 + SHR = b.rshift -- second argument is integer 0..31 + ROL = b.rol or b.lrotate -- second argument is integer 0..31 + ROR = b.ror or b.rrotate -- second argument is integer 0..31 + NOT = b.bnot -- only for LuaJIT + NORM = b.tobit -- only for LuaJIT + HEX = b.tohex -- returns string of 8 lowercase hexadecimal digits + assert(AND and OR and XOR and SHL and SHR and ROL and ROR and NOT, "Library '"..library_name.."' is incomplete") + XOR_BYTE = XOR -- XOR of two bytes (0..255) + +elseif branch == "EMUL" then + + -- Emulating 32-bit bitwise operations using 53-bit floating point arithmetic + + function SHL(x, n) + return (x * 2^n) % 2^32 + end + + function SHR(x, n) + x = x % 2^32 / 2^n + return x - x % 1 + end + + function ROL(x, n) + x = x % 2^32 * 2^n + local r = x % 2^32 + return r + (x - r) / 2^32 + end + + function ROR(x, n) + x = x % 2^32 / 2^n + local r = x % 1 + return r * 2^32 + (x - r) + end + + local AND_of_two_bytes = {[0] = 0} -- look-up table (256*256 entries) + local idx = 0 + for y = 0, 127 * 256, 256 do + for x = y, y + 127 do + x = AND_of_two_bytes[x] * 2 + AND_of_two_bytes[idx] = x + AND_of_two_bytes[idx + 1] = x + AND_of_two_bytes[idx + 256] = x + AND_of_two_bytes[idx + 257] = x + 1 + idx = idx + 2 + end + idx = idx + 256 + end + + local function and_or_xor(x, y, operation) + -- operation: nil = AND, 1 = OR, 2 = XOR + local x0 = x % 2^32 + local y0 = y % 2^32 + local rx = x0 % 256 + local ry = y0 % 256 + local res = AND_of_two_bytes[rx + ry * 256] + x = x0 - rx + y = (y0 - ry) / 256 + rx = x % 65536 + ry = y % 256 + res = res + AND_of_two_bytes[rx + ry] * 256 + x = (x - rx) / 256 + y = (y - ry) / 256 + rx = x % 65536 + y % 256 + res = res + AND_of_two_bytes[rx] * 65536 + res = res + AND_of_two_bytes[(x + y - rx) / 256] * 16777216 + if operation then + res = x0 + y0 - operation * res + end + return res + end + + function AND(x, y) + return and_or_xor(x, y) + end + + function OR(x, y) + return and_or_xor(x, y, 1) + end + + function XOR(x, y, z, t, u) -- 2..5 arguments + if z then + if t then + if u then + t = and_or_xor(t, u, 2) + end + z = and_or_xor(z, t, 2) + end + y = and_or_xor(y, z, 2) + end + return and_or_xor(x, y, 2) + end + + function XOR_BYTE(x, y) + return x + y - 2 * AND_of_two_bytes[x + y * 256] + end + +end + +HEX = HEX + or + pcall(string_format, "%x", 2^31) and + function (x) -- returns string of 8 lowercase hexadecimal digits + return string_format("%08x", x % 4294967296) + end + or + function (x) -- for OpenWrt's dialect of Lua + return string_format("%08x", (x + 2^31) % 2^32 - 2^31) + end + +local function XORA5(x, y) + return XOR(x, y or 0xA5A5A5A5) % 4294967296 +end + +local function create_array_of_lanes() + return {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +end + + +-------------------------------------------------------------------------------- +-- CREATING OPTIMIZED INNER LOOP +-------------------------------------------------------------------------------- + +-- Inner loop functions +local sha256_feed_64, sha512_feed_128, md5_feed_64, sha1_feed_64, keccak_feed, blake2s_feed_64, blake2b_feed_128, blake3_feed_64 + +-- Arrays of SHA-2 "magic numbers" (in "INT64" and "FFI" branches "*_lo" arrays contain 64-bit values) +local sha2_K_lo, sha2_K_hi, sha2_H_lo, sha2_H_hi, sha3_RC_lo, sha3_RC_hi = {}, {}, {}, {}, {}, {} +local sha2_H_ext256 = {[224] = {}, [256] = sha2_H_hi} +local sha2_H_ext512_lo, sha2_H_ext512_hi = {[384] = {}, [512] = sha2_H_lo}, {[384] = {}, [512] = sha2_H_hi} +local md5_K, md5_sha1_H = {}, {0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0} +local md5_next_shift = {0, 0, 0, 0, 0, 0, 0, 0, 28, 25, 26, 27, 0, 0, 10, 9, 11, 12, 0, 15, 16, 17, 18, 0, 20, 22, 23, 21} +local HEX64, lanes_index_base -- defined only for branches that internally use 64-bit integers: "INT64" and "FFI" +local common_W = {} -- temporary table shared between all calculations (to avoid creating new temporary table every time) +local common_W_blake2b, common_W_blake2s, v_for_blake2s_feed_64 = common_W, common_W, {} +local K_lo_modulo, hi_factor, hi_factor_keccak = 4294967296, 0, 0 +local sigma = { + { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, + { 15, 11, 5, 9, 10, 16, 14, 7, 2, 13, 1, 3, 12, 8, 6, 4 }, + { 12, 9, 13, 1, 6, 3, 16, 14, 11, 15, 4, 7, 8, 2, 10, 5 }, + { 8, 10, 4, 2, 14, 13, 12, 15, 3, 7, 6, 11, 5, 1, 16, 9 }, + { 10, 1, 6, 8, 3, 5, 11, 16, 15, 2, 12, 13, 7, 9, 4, 14 }, + { 3, 13, 7, 11, 1, 12, 9, 4, 5, 14, 8, 6, 16, 15, 2, 10 }, + { 13, 6, 2, 16, 15, 14, 5, 11, 1, 8, 7, 4, 10, 3, 9, 12 }, + { 14, 12, 8, 15, 13, 2, 4, 10, 6, 1, 16, 5, 9, 7, 3, 11 }, + { 7, 16, 15, 10, 12, 4, 1, 9, 13, 3, 14, 8, 2, 5, 11, 6 }, + { 11, 3, 9, 5, 8, 7, 2, 6, 16, 12, 10, 15, 4, 13, 14, 1 }, +}; sigma[11], sigma[12] = sigma[1], sigma[2] +local perm_blake3 = { + 1, 3, 4, 11, 13, 10, 12, 6, + 1, 3, 4, 11, 13, 10, + 2, 7, 5, 8, 14, 15, 16, 9, + 2, 7, 5, 8, 14, 15, +} + +local function build_keccak_format(elem) + local keccak_format = {} + for _, size in ipairs{1, 9, 13, 17, 18, 21} do + keccak_format[size] = "<"..string_rep(elem, size) + end + return keccak_format +end + + +if branch == "FFI" then + + local common_W_FFI_int32 = ffi.new("int32_t[?]", 80) -- 64 is enough for SHA256, but 80 is needed for SHA-1 + common_W_blake2s = common_W_FFI_int32 + v_for_blake2s_feed_64 = ffi.new("int32_t[?]", 16) + perm_blake3 = ffi.new("uint8_t[?]", #perm_blake3 + 1, 0, unpack(perm_blake3)) + for j = 1, 10 do + sigma[j] = ffi.new("uint8_t[?]", #sigma[j] + 1, 0, unpack(sigma[j])) + end; sigma[11], sigma[12] = sigma[1], sigma[2] + + + -- SHA256 implementation for "LuaJIT with FFI" branch + + function sha256_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W_FFI_int32, sha2_K_hi + for pos = offs, offs + size - 1, 64 do + for j = 0, 15 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) -- slow, but doesn't depend on endianness + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for j = 16, 63 do + local a, b = W[j-15], W[j-2] + W[j] = NORM( XOR(ROR(a, 7), ROL(a, 14), SHR(a, 3)) + XOR(ROL(b, 15), ROL(b, 13), SHR(b, 10)) + W[j-7] + W[j-16] ) + end + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for j = 0, 63, 8 do -- Thanks to Peter Cawley for this workaround (unroll the loop to avoid "PHI shuffling too complex" due to PHIs overlap) + local z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j] + K[j+1] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+1] + K[j+2] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+2] + K[j+3] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+3] + K[j+4] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+4] + K[j+5] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+5] + K[j+6] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+6] + K[j+7] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(g, AND(e, XOR(f, g))) + XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + (W[j+7] + K[j+8] + h) ) + h, g, f, e = g, f, e, NORM( d + z ) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + end + H[1], H[2], H[3], H[4] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]) + H[5], H[6], H[7], H[8] = NORM(e + H[5]), NORM(f + H[6]), NORM(g + H[7]), NORM(h + H[8]) + end + end + + + local common_W_FFI_int64 = ffi.new("int64_t[?]", 80) + common_W_blake2b = common_W_FFI_int64 + local int64 = ffi.typeof"int64_t" + local int32 = ffi.typeof"int32_t" + local uint32 = ffi.typeof"uint32_t" + hi_factor = int64(2^32) + + if is_LuaJIT_21 then -- LuaJIT 2.1 supports bitwise 64-bit operations + + local AND64, OR64, XOR64, NOT64, SHL64, SHR64, ROL64, ROR64 -- introducing synonyms for better code readability + = AND, OR, XOR, NOT, SHL, SHR, ROL, ROR + HEX64 = HEX + + + -- BLAKE2b implementation for "LuaJIT 2.1 + FFI" branch + + do + local v = ffi.new("int64_t[?]", 16) + local W = common_W_blake2b + + local function G(a, b, c, d, k1, k2) + local va, vb, vc, vd = v[a], v[b], v[c], v[d] + va = W[k1] + (va + vb) + vd = ROR64(XOR64(vd, va), 32) + vc = vc + vd + vb = ROR64(XOR64(vb, vc), 24) + va = W[k2] + (va + vb) + vd = ROR64(XOR64(vd, va), 16) + vc = vc + vd + vb = ROL64(XOR64(vb, vc), 1) + v[a], v[b], v[c], v[d] = va, vb, vc, vd + end + + function blake2b_feed_128(H, _, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs, offs + size - 1, 128 do + if str then + for j = 1, 16 do + pos = pos + 8 + local a, b, c, d, e, f, g, h = byte(str, pos - 7, pos) + W[j] = XOR64(OR(SHL(h, 24), SHL(g, 16), SHL(f, 8), e) * int64(2^32), uint32(int32(OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a)))) + end + end + v[0x0], v[0x1], v[0x2], v[0x3], v[0x4], v[0x5], v[0x6], v[0x7] = h1, h2, h3, h4, h5, h6, h7, h8 + v[0x8], v[0x9], v[0xA], v[0xB], v[0xD], v[0xE], v[0xF] = sha2_H_lo[1], sha2_H_lo[2], sha2_H_lo[3], sha2_H_lo[4], sha2_H_lo[6], sha2_H_lo[7], sha2_H_lo[8] + bytes_compressed = bytes_compressed + (last_block_size or 128) + v[0xC] = XOR64(sha2_H_lo[5], bytes_compressed) -- t0 = low_8_bytes(bytes_compressed) + -- t1 = high_8_bytes(bytes_compressed) = 0, message length is always below 2^53 bytes + if last_block_size then -- flag f0 + v[0xE] = NOT64(v[0xE]) + end + if is_last_node then -- flag f1 + v[0xF] = NOT64(v[0xF]) + end + for j = 1, 12 do + local row = sigma[j] + G(0, 4, 8, 12, row[ 1], row[ 2]) + G(1, 5, 9, 13, row[ 3], row[ 4]) + G(2, 6, 10, 14, row[ 5], row[ 6]) + G(3, 7, 11, 15, row[ 7], row[ 8]) + G(0, 5, 10, 15, row[ 9], row[10]) + G(1, 6, 11, 12, row[11], row[12]) + G(2, 7, 8, 13, row[13], row[14]) + G(3, 4, 9, 14, row[15], row[16]) + end + h1 = XOR64(h1, v[0x0], v[0x8]) + h2 = XOR64(h2, v[0x1], v[0x9]) + h3 = XOR64(h3, v[0x2], v[0xA]) + h4 = XOR64(h4, v[0x3], v[0xB]) + h5 = XOR64(h5, v[0x4], v[0xC]) + h6 = XOR64(h6, v[0x5], v[0xD]) + h7 = XOR64(h7, v[0x6], v[0xE]) + h8 = XOR64(h8, v[0x7], v[0xF]) + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + end + + + -- SHA-3 implementation for "LuaJIT 2.1 + FFI" branch + + local arr64_t = ffi.typeof"int64_t[?]" + -- lanes array is indexed from 0 + lanes_index_base = 0 + hi_factor_keccak = int64(2^32) + + function create_array_of_lanes() + return arr64_t(30) -- 25 + 5 for temporary usage + end + + function keccak_feed(lanes, _, str, offs, size, block_size_in_bytes) + -- offs >= 0, size >= 0, size is multiple of block_size_in_bytes, block_size_in_bytes is positive multiple of 8 + local RC = sha3_RC_lo + local qwords_qty = SHR(block_size_in_bytes, 3) + for pos = offs, offs + size - 1, block_size_in_bytes do + for j = 0, qwords_qty - 1 do + pos = pos + 8 + local h, g, f, e, d, c, b, a = byte(str, pos - 7, pos) -- slow, but doesn't depend on endianness + lanes[j] = XOR64(lanes[j], OR64(OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) * int64(2^32), uint32(int32(OR(SHL(e, 24), SHL(f, 16), SHL(g, 8), h))))) + end + for round_idx = 1, 24 do + for j = 0, 4 do + lanes[25 + j] = XOR64(lanes[j], lanes[j+5], lanes[j+10], lanes[j+15], lanes[j+20]) + end + local D = XOR64(lanes[25], ROL64(lanes[27], 1)) + lanes[1], lanes[6], lanes[11], lanes[16] = ROL64(XOR64(D, lanes[6]), 44), ROL64(XOR64(D, lanes[16]), 45), ROL64(XOR64(D, lanes[1]), 1), ROL64(XOR64(D, lanes[11]), 10) + lanes[21] = ROL64(XOR64(D, lanes[21]), 2) + D = XOR64(lanes[26], ROL64(lanes[28], 1)) + lanes[2], lanes[7], lanes[12], lanes[22] = ROL64(XOR64(D, lanes[12]), 43), ROL64(XOR64(D, lanes[22]), 61), ROL64(XOR64(D, lanes[7]), 6), ROL64(XOR64(D, lanes[2]), 62) + lanes[17] = ROL64(XOR64(D, lanes[17]), 15) + D = XOR64(lanes[27], ROL64(lanes[29], 1)) + lanes[3], lanes[8], lanes[18], lanes[23] = ROL64(XOR64(D, lanes[18]), 21), ROL64(XOR64(D, lanes[3]), 28), ROL64(XOR64(D, lanes[23]), 56), ROL64(XOR64(D, lanes[8]), 55) + lanes[13] = ROL64(XOR64(D, lanes[13]), 25) + D = XOR64(lanes[28], ROL64(lanes[25], 1)) + lanes[4], lanes[14], lanes[19], lanes[24] = ROL64(XOR64(D, lanes[24]), 14), ROL64(XOR64(D, lanes[19]), 8), ROL64(XOR64(D, lanes[4]), 27), ROL64(XOR64(D, lanes[14]), 39) + lanes[9] = ROL64(XOR64(D, lanes[9]), 20) + D = XOR64(lanes[29], ROL64(lanes[26], 1)) + lanes[5], lanes[10], lanes[15], lanes[20] = ROL64(XOR64(D, lanes[10]), 3), ROL64(XOR64(D, lanes[20]), 18), ROL64(XOR64(D, lanes[5]), 36), ROL64(XOR64(D, lanes[15]), 41) + lanes[0] = XOR64(D, lanes[0]) + lanes[0], lanes[1], lanes[2], lanes[3], lanes[4] = XOR64(lanes[0], AND64(NOT64(lanes[1]), lanes[2]), RC[round_idx]), XOR64(lanes[1], AND64(NOT64(lanes[2]), lanes[3])), XOR64(lanes[2], AND64(NOT64(lanes[3]), lanes[4])), XOR64(lanes[3], AND64(NOT64(lanes[4]), lanes[0])), XOR64(lanes[4], AND64(NOT64(lanes[0]), lanes[1])) + lanes[5], lanes[6], lanes[7], lanes[8], lanes[9] = XOR64(lanes[8], AND64(NOT64(lanes[9]), lanes[5])), XOR64(lanes[9], AND64(NOT64(lanes[5]), lanes[6])), XOR64(lanes[5], AND64(NOT64(lanes[6]), lanes[7])), XOR64(lanes[6], AND64(NOT64(lanes[7]), lanes[8])), XOR64(lanes[7], AND64(NOT64(lanes[8]), lanes[9])) + lanes[10], lanes[11], lanes[12], lanes[13], lanes[14] = XOR64(lanes[11], AND64(NOT64(lanes[12]), lanes[13])), XOR64(lanes[12], AND64(NOT64(lanes[13]), lanes[14])), XOR64(lanes[13], AND64(NOT64(lanes[14]), lanes[10])), XOR64(lanes[14], AND64(NOT64(lanes[10]), lanes[11])), XOR64(lanes[10], AND64(NOT64(lanes[11]), lanes[12])) + lanes[15], lanes[16], lanes[17], lanes[18], lanes[19] = XOR64(lanes[19], AND64(NOT64(lanes[15]), lanes[16])), XOR64(lanes[15], AND64(NOT64(lanes[16]), lanes[17])), XOR64(lanes[16], AND64(NOT64(lanes[17]), lanes[18])), XOR64(lanes[17], AND64(NOT64(lanes[18]), lanes[19])), XOR64(lanes[18], AND64(NOT64(lanes[19]), lanes[15])) + lanes[20], lanes[21], lanes[22], lanes[23], lanes[24] = XOR64(lanes[22], AND64(NOT64(lanes[23]), lanes[24])), XOR64(lanes[23], AND64(NOT64(lanes[24]), lanes[20])), XOR64(lanes[24], AND64(NOT64(lanes[20]), lanes[21])), XOR64(lanes[20], AND64(NOT64(lanes[21]), lanes[22])), XOR64(lanes[21], AND64(NOT64(lanes[22]), lanes[23])) + end + end + end + + + local A5_long = 0xA5A5A5A5 * int64(2^32 + 1) -- It's impossible to use constant 0xA5A5A5A5A5A5A5A5LL because it will raise syntax error on other Lua versions + + function XORA5(long, long2) + return XOR64(long, long2 or A5_long) + end + + + -- SHA512 implementation for "LuaJIT 2.1 + FFI" branch + + function sha512_feed_128(H, _, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + local W, K = common_W_FFI_int64, sha2_K_lo + for pos = offs, offs + size - 1, 128 do + for j = 0, 15 do + pos = pos + 8 + local a, b, c, d, e, f, g, h = byte(str, pos - 7, pos) -- slow, but doesn't depend on endianness + W[j] = OR64(OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) * int64(2^32), uint32(int32(OR(SHL(e, 24), SHL(f, 16), SHL(g, 8), h)))) + end + for j = 16, 79 do + local a, b = W[j-15], W[j-2] + W[j] = XOR64(ROR64(a, 1), ROR64(a, 8), SHR64(a, 7)) + XOR64(ROR64(b, 19), ROL64(b, 3), SHR64(b, 6)) + W[j-7] + W[j-16] + end + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for j = 0, 79, 8 do + local z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+1] + W[j] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+2] + W[j+1] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+3] + W[j+2] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+4] + W[j+3] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+5] + W[j+4] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+6] + W[j+5] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+7] + W[j+6] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + z = XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + XOR64(g, AND64(e, XOR64(f, g))) + h + K[j+8] + W[j+7] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + z + end + H[1] = a + H[1] + H[2] = b + H[2] + H[3] = c + H[3] + H[4] = d + H[4] + H[5] = e + H[5] + H[6] = f + H[6] + H[7] = g + H[7] + H[8] = h + H[8] + end + end + + else -- LuaJIT 2.0 doesn't support 64-bit bitwise operations + + local U = ffi.new("union{int64_t i64; struct{int32_t "..(ffi.abi("le") and "lo, hi" or "hi, lo")..";} i32;}[3]") + -- this array of unions is used for fast splitting int64 into int32_high and int32_low + + -- "xorrific" 64-bit functions :-) + -- int64 input is splitted into two int32 parts, some bitwise 32-bit operations are performed, finally the result is converted to int64 + -- these functions are needed because bit.* functions in LuaJIT 2.0 don't work with int64_t + + local function XORROR64_1(a) + -- return XOR64(ROR64(a, 1), ROR64(a, 8), SHR64(a, 7)) + U[0].i64 = a + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local t_lo = XOR(SHR(a_lo, 1), SHL(a_hi, 31), SHR(a_lo, 8), SHL(a_hi, 24), SHR(a_lo, 7), SHL(a_hi, 25)) + local t_hi = XOR(SHR(a_hi, 1), SHL(a_lo, 31), SHR(a_hi, 8), SHL(a_lo, 24), SHR(a_hi, 7)) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + local function XORROR64_2(b) + -- return XOR64(ROR64(b, 19), ROL64(b, 3), SHR64(b, 6)) + U[0].i64 = b + local b_lo, b_hi = U[0].i32.lo, U[0].i32.hi + local u_lo = XOR(SHR(b_lo, 19), SHL(b_hi, 13), SHL(b_lo, 3), SHR(b_hi, 29), SHR(b_lo, 6), SHL(b_hi, 26)) + local u_hi = XOR(SHR(b_hi, 19), SHL(b_lo, 13), SHL(b_hi, 3), SHR(b_lo, 29), SHR(b_hi, 6)) + return u_hi * int64(2^32) + uint32(int32(u_lo)) + end + + local function XORROR64_3(e) + -- return XOR64(ROR64(e, 14), ROR64(e, 18), ROL64(e, 23)) + U[0].i64 = e + local e_lo, e_hi = U[0].i32.lo, U[0].i32.hi + local u_lo = XOR(SHR(e_lo, 14), SHL(e_hi, 18), SHR(e_lo, 18), SHL(e_hi, 14), SHL(e_lo, 23), SHR(e_hi, 9)) + local u_hi = XOR(SHR(e_hi, 14), SHL(e_lo, 18), SHR(e_hi, 18), SHL(e_lo, 14), SHL(e_hi, 23), SHR(e_lo, 9)) + return u_hi * int64(2^32) + uint32(int32(u_lo)) + end + + local function XORROR64_6(a) + -- return XOR64(ROR64(a, 28), ROL64(a, 25), ROL64(a, 30)) + U[0].i64 = a + local b_lo, b_hi = U[0].i32.lo, U[0].i32.hi + local u_lo = XOR(SHR(b_lo, 28), SHL(b_hi, 4), SHL(b_lo, 30), SHR(b_hi, 2), SHL(b_lo, 25), SHR(b_hi, 7)) + local u_hi = XOR(SHR(b_hi, 28), SHL(b_lo, 4), SHL(b_hi, 30), SHR(b_lo, 2), SHL(b_hi, 25), SHR(b_lo, 7)) + return u_hi * int64(2^32) + uint32(int32(u_lo)) + end + + local function XORROR64_4(e, f, g) + -- return XOR64(g, AND64(e, XOR64(f, g))) + U[0].i64 = f + U[1].i64 = g + U[2].i64 = e + local f_lo, f_hi = U[0].i32.lo, U[0].i32.hi + local g_lo, g_hi = U[1].i32.lo, U[1].i32.hi + local e_lo, e_hi = U[2].i32.lo, U[2].i32.hi + local result_lo = XOR(g_lo, AND(e_lo, XOR(f_lo, g_lo))) + local result_hi = XOR(g_hi, AND(e_hi, XOR(f_hi, g_hi))) + return result_hi * int64(2^32) + uint32(int32(result_lo)) + end + + local function XORROR64_5(a, b, c) + -- return XOR64(AND64(XOR64(a, b), c), AND64(a, b)) + U[0].i64 = a + U[1].i64 = b + U[2].i64 = c + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local c_lo, c_hi = U[2].i32.lo, U[2].i32.hi + local result_lo = XOR(AND(XOR(a_lo, b_lo), c_lo), AND(a_lo, b_lo)) + local result_hi = XOR(AND(XOR(a_hi, b_hi), c_hi), AND(a_hi, b_hi)) + return result_hi * int64(2^32) + uint32(int32(result_lo)) + end + + local function XORROR64_7(a, b, m) + -- return ROR64(XOR64(a, b), m), m = 1..31 + U[0].i64 = a + U[1].i64 = b + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local c_lo, c_hi = XOR(a_lo, b_lo), XOR(a_hi, b_hi) + local t_lo = XOR(SHR(c_lo, m), SHL(c_hi, -m)) + local t_hi = XOR(SHR(c_hi, m), SHL(c_lo, -m)) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + local function XORROR64_8(a, b) + -- return ROL64(XOR64(a, b), 1) + U[0].i64 = a + U[1].i64 = b + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local c_lo, c_hi = XOR(a_lo, b_lo), XOR(a_hi, b_hi) + local t_lo = XOR(SHL(c_lo, 1), SHR(c_hi, 31)) + local t_hi = XOR(SHL(c_hi, 1), SHR(c_lo, 31)) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + local function XORROR64_9(a, b) + -- return ROR64(XOR64(a, b), 32) + U[0].i64 = a + U[1].i64 = b + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local t_hi, t_lo = XOR(a_lo, b_lo), XOR(a_hi, b_hi) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + local function XOR64(a, b) + -- return XOR64(a, b) + U[0].i64 = a + U[1].i64 = b + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local t_lo, t_hi = XOR(a_lo, b_lo), XOR(a_hi, b_hi) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + local function XORROR64_11(a, b, c) + -- return XOR64(a, b, c) + U[0].i64 = a + U[1].i64 = b + U[2].i64 = c + local a_lo, a_hi = U[0].i32.lo, U[0].i32.hi + local b_lo, b_hi = U[1].i32.lo, U[1].i32.hi + local c_lo, c_hi = U[2].i32.lo, U[2].i32.hi + local t_lo, t_hi = XOR(a_lo, b_lo, c_lo), XOR(a_hi, b_hi, c_hi) + return t_hi * int64(2^32) + uint32(int32(t_lo)) + end + + function XORA5(long, long2) + -- return XOR64(long, long2 or 0xA5A5A5A5A5A5A5A5) + U[0].i64 = long + local lo32, hi32 = U[0].i32.lo, U[0].i32.hi + local long2_lo, long2_hi = 0xA5A5A5A5, 0xA5A5A5A5 + if long2 then + U[1].i64 = long2 + long2_lo, long2_hi = U[1].i32.lo, U[1].i32.hi + end + lo32 = XOR(lo32, long2_lo) + hi32 = XOR(hi32, long2_hi) + return hi32 * int64(2^32) + uint32(int32(lo32)) + end + + function HEX64(long) + U[0].i64 = long + return HEX(U[0].i32.hi)..HEX(U[0].i32.lo) + end + + + -- SHA512 implementation for "LuaJIT 2.0 + FFI" branch + + function sha512_feed_128(H, _, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + local W, K = common_W_FFI_int64, sha2_K_lo + for pos = offs, offs + size - 1, 128 do + for j = 0, 15 do + pos = pos + 8 + local a, b, c, d, e, f, g, h = byte(str, pos - 7, pos) -- slow, but doesn't depend on endianness + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) * int64(2^32) + uint32(int32(OR(SHL(e, 24), SHL(f, 16), SHL(g, 8), h))) + end + for j = 16, 79 do + W[j] = XORROR64_1(W[j-15]) + XORROR64_2(W[j-2]) + W[j-7] + W[j-16] + end + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for j = 0, 79, 8 do + local z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+1] + W[j] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+2] + W[j+1] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+3] + W[j+2] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+4] + W[j+3] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+5] + W[j+4] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+6] + W[j+5] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+7] + W[j+6] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + z = XORROR64_3(e) + XORROR64_4(e, f, g) + h + K[j+8] + W[j+7] + h, g, f, e = g, f, e, z + d + d, c, b, a = c, b, a, XORROR64_5(a, b, c) + XORROR64_6(a) + z + end + H[1] = a + H[1] + H[2] = b + H[2] + H[3] = c + H[3] + H[4] = d + H[4] + H[5] = e + H[5] + H[6] = f + H[6] + H[7] = g + H[7] + H[8] = h + H[8] + end + end + + + -- BLAKE2b implementation for "LuaJIT 2.0 + FFI" branch + + do + local v = ffi.new("int64_t[?]", 16) + local W = common_W_blake2b + + local function G(a, b, c, d, k1, k2) + local va, vb, vc, vd = v[a], v[b], v[c], v[d] + va = W[k1] + (va + vb) + vd = XORROR64_9(vd, va) + vc = vc + vd + vb = XORROR64_7(vb, vc, 24) + va = W[k2] + (va + vb) + vd = XORROR64_7(vd, va, 16) + vc = vc + vd + vb = XORROR64_8(vb, vc) + v[a], v[b], v[c], v[d] = va, vb, vc, vd + end + + function blake2b_feed_128(H, _, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs, offs + size - 1, 128 do + if str then + for j = 1, 16 do + pos = pos + 8 + local a, b, c, d, e, f, g, h = byte(str, pos - 7, pos) + W[j] = XOR64(OR(SHL(h, 24), SHL(g, 16), SHL(f, 8), e) * int64(2^32), uint32(int32(OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a)))) + end + end + v[0x0], v[0x1], v[0x2], v[0x3], v[0x4], v[0x5], v[0x6], v[0x7] = h1, h2, h3, h4, h5, h6, h7, h8 + v[0x8], v[0x9], v[0xA], v[0xB], v[0xD], v[0xE], v[0xF] = sha2_H_lo[1], sha2_H_lo[2], sha2_H_lo[3], sha2_H_lo[4], sha2_H_lo[6], sha2_H_lo[7], sha2_H_lo[8] + bytes_compressed = bytes_compressed + (last_block_size or 128) + v[0xC] = XOR64(sha2_H_lo[5], bytes_compressed) -- t0 = low_8_bytes(bytes_compressed) + -- t1 = high_8_bytes(bytes_compressed) = 0, message length is always below 2^53 bytes + if last_block_size then -- flag f0 + v[0xE] = -1 - v[0xE] + end + if is_last_node then -- flag f1 + v[0xF] = -1 - v[0xF] + end + for j = 1, 12 do + local row = sigma[j] + G(0, 4, 8, 12, row[ 1], row[ 2]) + G(1, 5, 9, 13, row[ 3], row[ 4]) + G(2, 6, 10, 14, row[ 5], row[ 6]) + G(3, 7, 11, 15, row[ 7], row[ 8]) + G(0, 5, 10, 15, row[ 9], row[10]) + G(1, 6, 11, 12, row[11], row[12]) + G(2, 7, 8, 13, row[13], row[14]) + G(3, 4, 9, 14, row[15], row[16]) + end + h1 = XORROR64_11(h1, v[0x0], v[0x8]) + h2 = XORROR64_11(h2, v[0x1], v[0x9]) + h3 = XORROR64_11(h3, v[0x2], v[0xA]) + h4 = XORROR64_11(h4, v[0x3], v[0xB]) + h5 = XORROR64_11(h5, v[0x4], v[0xC]) + h6 = XORROR64_11(h6, v[0x5], v[0xD]) + h7 = XORROR64_11(h7, v[0x6], v[0xE]) + h8 = XORROR64_11(h8, v[0x7], v[0xF]) + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + end + + end + + + -- MD5 implementation for "LuaJIT with FFI" branch + + function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W_FFI_int32, md5_K + for pos = offs, offs + size - 1, 64 do + for j = 0, 15 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) -- slow, but doesn't depend on endianness + W[j] = OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a) + end + local a, b, c, d = H[1], H[2], H[3], H[4] + for j = 0, 15, 4 do + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+1] + W[j ] + a), 7) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+2] + W[j+1] + a), 12) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+3] + W[j+2] + a), 17) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+4] + W[j+3] + a), 22) + b) + end + for j = 16, 31, 4 do + local g = 5*j + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+1] + W[AND(g + 1, 15)] + a), 5) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+2] + W[AND(g + 6, 15)] + a), 9) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+3] + W[AND(g - 5, 15)] + a), 14) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+4] + W[AND(g , 15)] + a), 20) + b) + end + for j = 32, 47, 4 do + local g = 3*j + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+1] + W[AND(g + 5, 15)] + a), 4) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+2] + W[AND(g + 8, 15)] + a), 11) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+3] + W[AND(g - 5, 15)] + a), 16) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+4] + W[AND(g - 2, 15)] + a), 23) + b) + end + for j = 48, 63, 4 do + local g = 7*j + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+1] + W[AND(g , 15)] + a), 6) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+2] + W[AND(g + 7, 15)] + a), 10) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+3] + W[AND(g - 2, 15)] + a), 15) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+4] + W[AND(g + 5, 15)] + a), 21) + b) + end + H[1], H[2], H[3], H[4] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]) + end + end + + + -- SHA-1 implementation for "LuaJIT with FFI" branch + + function sha1_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W_FFI_int32 + for pos = offs, offs + size - 1, 64 do + for j = 0, 15 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) -- slow, but doesn't depend on endianness + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for j = 16, 79 do + W[j] = ROL(XOR(W[j-3], W[j-8], W[j-14], W[j-16]), 1) + end + local a, b, c, d, e = H[1], H[2], H[3], H[4], H[5] + for j = 0, 19, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j] + 0x5A827999 + e)) -- constant = floor(2^30 * sqrt(2)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+1] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+2] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+3] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+4] + 0x5A827999 + e)) + end + for j = 20, 39, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j] + 0x6ED9EBA1 + e)) -- 2^30 * sqrt(3) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+1] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+2] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+3] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+4] + 0x6ED9EBA1 + e)) + end + for j = 40, 59, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j] + 0x8F1BBCDC + e)) -- 2^30 * sqrt(5) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+1] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+2] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+3] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+4] + 0x8F1BBCDC + e)) + end + for j = 60, 79, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j] + 0xCA62C1D6 + e)) -- 2^30 * sqrt(10) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+1] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+2] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+3] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+4] + 0xCA62C1D6 + e)) + end + H[1], H[2], H[3], H[4], H[5] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]), NORM(e + H[5]) + end + end + +end + + +if branch == "FFI" and not is_LuaJIT_21 or branch == "LJ" then + + if branch == "FFI" then + local arr32_t = ffi.typeof"int32_t[?]" + + function create_array_of_lanes() + return arr32_t(31) -- 25 + 5 + 1 (due to 1-based indexing) + end + + end + + + -- SHA-3 implementation for "LuaJIT 2.0 + FFI" and "LuaJIT without FFI" branches + + function keccak_feed(lanes_lo, lanes_hi, str, offs, size, block_size_in_bytes) + -- offs >= 0, size >= 0, size is multiple of block_size_in_bytes, block_size_in_bytes is positive multiple of 8 + local RC_lo, RC_hi = sha3_RC_lo, sha3_RC_hi + local qwords_qty = SHR(block_size_in_bytes, 3) + for pos = offs, offs + size - 1, block_size_in_bytes do + for j = 1, qwords_qty do + local a, b, c, d = byte(str, pos + 1, pos + 4) + lanes_lo[j] = XOR(lanes_lo[j], OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a)) + pos = pos + 8 + a, b, c, d = byte(str, pos - 3, pos) + lanes_hi[j] = XOR(lanes_hi[j], OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a)) + end + for round_idx = 1, 24 do + for j = 1, 5 do + lanes_lo[25 + j] = XOR(lanes_lo[j], lanes_lo[j + 5], lanes_lo[j + 10], lanes_lo[j + 15], lanes_lo[j + 20]) + end + for j = 1, 5 do + lanes_hi[25 + j] = XOR(lanes_hi[j], lanes_hi[j + 5], lanes_hi[j + 10], lanes_hi[j + 15], lanes_hi[j + 20]) + end + local D_lo = XOR(lanes_lo[26], SHL(lanes_lo[28], 1), SHR(lanes_hi[28], 31)) + local D_hi = XOR(lanes_hi[26], SHL(lanes_hi[28], 1), SHR(lanes_lo[28], 31)) + lanes_lo[2], lanes_hi[2], lanes_lo[7], lanes_hi[7], lanes_lo[12], lanes_hi[12], lanes_lo[17], lanes_hi[17] = XOR(SHR(XOR(D_lo, lanes_lo[7]), 20), SHL(XOR(D_hi, lanes_hi[7]), 12)), XOR(SHR(XOR(D_hi, lanes_hi[7]), 20), SHL(XOR(D_lo, lanes_lo[7]), 12)), XOR(SHR(XOR(D_lo, lanes_lo[17]), 19), SHL(XOR(D_hi, lanes_hi[17]), 13)), XOR(SHR(XOR(D_hi, lanes_hi[17]), 19), SHL(XOR(D_lo, lanes_lo[17]), 13)), XOR(SHL(XOR(D_lo, lanes_lo[2]), 1), SHR(XOR(D_hi, lanes_hi[2]), 31)), XOR(SHL(XOR(D_hi, lanes_hi[2]), 1), SHR(XOR(D_lo, lanes_lo[2]), 31)), XOR(SHL(XOR(D_lo, lanes_lo[12]), 10), SHR(XOR(D_hi, lanes_hi[12]), 22)), XOR(SHL(XOR(D_hi, lanes_hi[12]), 10), SHR(XOR(D_lo, lanes_lo[12]), 22)) + local L, H = XOR(D_lo, lanes_lo[22]), XOR(D_hi, lanes_hi[22]) + lanes_lo[22], lanes_hi[22] = XOR(SHL(L, 2), SHR(H, 30)), XOR(SHL(H, 2), SHR(L, 30)) + D_lo = XOR(lanes_lo[27], SHL(lanes_lo[29], 1), SHR(lanes_hi[29], 31)) + D_hi = XOR(lanes_hi[27], SHL(lanes_hi[29], 1), SHR(lanes_lo[29], 31)) + lanes_lo[3], lanes_hi[3], lanes_lo[8], lanes_hi[8], lanes_lo[13], lanes_hi[13], lanes_lo[23], lanes_hi[23] = XOR(SHR(XOR(D_lo, lanes_lo[13]), 21), SHL(XOR(D_hi, lanes_hi[13]), 11)), XOR(SHR(XOR(D_hi, lanes_hi[13]), 21), SHL(XOR(D_lo, lanes_lo[13]), 11)), XOR(SHR(XOR(D_lo, lanes_lo[23]), 3), SHL(XOR(D_hi, lanes_hi[23]), 29)), XOR(SHR(XOR(D_hi, lanes_hi[23]), 3), SHL(XOR(D_lo, lanes_lo[23]), 29)), XOR(SHL(XOR(D_lo, lanes_lo[8]), 6), SHR(XOR(D_hi, lanes_hi[8]), 26)), XOR(SHL(XOR(D_hi, lanes_hi[8]), 6), SHR(XOR(D_lo, lanes_lo[8]), 26)), XOR(SHR(XOR(D_lo, lanes_lo[3]), 2), SHL(XOR(D_hi, lanes_hi[3]), 30)), XOR(SHR(XOR(D_hi, lanes_hi[3]), 2), SHL(XOR(D_lo, lanes_lo[3]), 30)) + L, H = XOR(D_lo, lanes_lo[18]), XOR(D_hi, lanes_hi[18]) + lanes_lo[18], lanes_hi[18] = XOR(SHL(L, 15), SHR(H, 17)), XOR(SHL(H, 15), SHR(L, 17)) + D_lo = XOR(lanes_lo[28], SHL(lanes_lo[30], 1), SHR(lanes_hi[30], 31)) + D_hi = XOR(lanes_hi[28], SHL(lanes_hi[30], 1), SHR(lanes_lo[30], 31)) + lanes_lo[4], lanes_hi[4], lanes_lo[9], lanes_hi[9], lanes_lo[19], lanes_hi[19], lanes_lo[24], lanes_hi[24] = XOR(SHL(XOR(D_lo, lanes_lo[19]), 21), SHR(XOR(D_hi, lanes_hi[19]), 11)), XOR(SHL(XOR(D_hi, lanes_hi[19]), 21), SHR(XOR(D_lo, lanes_lo[19]), 11)), XOR(SHL(XOR(D_lo, lanes_lo[4]), 28), SHR(XOR(D_hi, lanes_hi[4]), 4)), XOR(SHL(XOR(D_hi, lanes_hi[4]), 28), SHR(XOR(D_lo, lanes_lo[4]), 4)), XOR(SHR(XOR(D_lo, lanes_lo[24]), 8), SHL(XOR(D_hi, lanes_hi[24]), 24)), XOR(SHR(XOR(D_hi, lanes_hi[24]), 8), SHL(XOR(D_lo, lanes_lo[24]), 24)), XOR(SHR(XOR(D_lo, lanes_lo[9]), 9), SHL(XOR(D_hi, lanes_hi[9]), 23)), XOR(SHR(XOR(D_hi, lanes_hi[9]), 9), SHL(XOR(D_lo, lanes_lo[9]), 23)) + L, H = XOR(D_lo, lanes_lo[14]), XOR(D_hi, lanes_hi[14]) + lanes_lo[14], lanes_hi[14] = XOR(SHL(L, 25), SHR(H, 7)), XOR(SHL(H, 25), SHR(L, 7)) + D_lo = XOR(lanes_lo[29], SHL(lanes_lo[26], 1), SHR(lanes_hi[26], 31)) + D_hi = XOR(lanes_hi[29], SHL(lanes_hi[26], 1), SHR(lanes_lo[26], 31)) + lanes_lo[5], lanes_hi[5], lanes_lo[15], lanes_hi[15], lanes_lo[20], lanes_hi[20], lanes_lo[25], lanes_hi[25] = XOR(SHL(XOR(D_lo, lanes_lo[25]), 14), SHR(XOR(D_hi, lanes_hi[25]), 18)), XOR(SHL(XOR(D_hi, lanes_hi[25]), 14), SHR(XOR(D_lo, lanes_lo[25]), 18)), XOR(SHL(XOR(D_lo, lanes_lo[20]), 8), SHR(XOR(D_hi, lanes_hi[20]), 24)), XOR(SHL(XOR(D_hi, lanes_hi[20]), 8), SHR(XOR(D_lo, lanes_lo[20]), 24)), XOR(SHL(XOR(D_lo, lanes_lo[5]), 27), SHR(XOR(D_hi, lanes_hi[5]), 5)), XOR(SHL(XOR(D_hi, lanes_hi[5]), 27), SHR(XOR(D_lo, lanes_lo[5]), 5)), XOR(SHR(XOR(D_lo, lanes_lo[15]), 25), SHL(XOR(D_hi, lanes_hi[15]), 7)), XOR(SHR(XOR(D_hi, lanes_hi[15]), 25), SHL(XOR(D_lo, lanes_lo[15]), 7)) + L, H = XOR(D_lo, lanes_lo[10]), XOR(D_hi, lanes_hi[10]) + lanes_lo[10], lanes_hi[10] = XOR(SHL(L, 20), SHR(H, 12)), XOR(SHL(H, 20), SHR(L, 12)) + D_lo = XOR(lanes_lo[30], SHL(lanes_lo[27], 1), SHR(lanes_hi[27], 31)) + D_hi = XOR(lanes_hi[30], SHL(lanes_hi[27], 1), SHR(lanes_lo[27], 31)) + lanes_lo[6], lanes_hi[6], lanes_lo[11], lanes_hi[11], lanes_lo[16], lanes_hi[16], lanes_lo[21], lanes_hi[21] = XOR(SHL(XOR(D_lo, lanes_lo[11]), 3), SHR(XOR(D_hi, lanes_hi[11]), 29)), XOR(SHL(XOR(D_hi, lanes_hi[11]), 3), SHR(XOR(D_lo, lanes_lo[11]), 29)), XOR(SHL(XOR(D_lo, lanes_lo[21]), 18), SHR(XOR(D_hi, lanes_hi[21]), 14)), XOR(SHL(XOR(D_hi, lanes_hi[21]), 18), SHR(XOR(D_lo, lanes_lo[21]), 14)), XOR(SHR(XOR(D_lo, lanes_lo[6]), 28), SHL(XOR(D_hi, lanes_hi[6]), 4)), XOR(SHR(XOR(D_hi, lanes_hi[6]), 28), SHL(XOR(D_lo, lanes_lo[6]), 4)), XOR(SHR(XOR(D_lo, lanes_lo[16]), 23), SHL(XOR(D_hi, lanes_hi[16]), 9)), XOR(SHR(XOR(D_hi, lanes_hi[16]), 23), SHL(XOR(D_lo, lanes_lo[16]), 9)) + lanes_lo[1], lanes_hi[1] = XOR(D_lo, lanes_lo[1]), XOR(D_hi, lanes_hi[1]) + lanes_lo[1], lanes_lo[2], lanes_lo[3], lanes_lo[4], lanes_lo[5] = XOR(lanes_lo[1], AND(NOT(lanes_lo[2]), lanes_lo[3]), RC_lo[round_idx]), XOR(lanes_lo[2], AND(NOT(lanes_lo[3]), lanes_lo[4])), XOR(lanes_lo[3], AND(NOT(lanes_lo[4]), lanes_lo[5])), XOR(lanes_lo[4], AND(NOT(lanes_lo[5]), lanes_lo[1])), XOR(lanes_lo[5], AND(NOT(lanes_lo[1]), lanes_lo[2])) + lanes_lo[6], lanes_lo[7], lanes_lo[8], lanes_lo[9], lanes_lo[10] = XOR(lanes_lo[9], AND(NOT(lanes_lo[10]), lanes_lo[6])), XOR(lanes_lo[10], AND(NOT(lanes_lo[6]), lanes_lo[7])), XOR(lanes_lo[6], AND(NOT(lanes_lo[7]), lanes_lo[8])), XOR(lanes_lo[7], AND(NOT(lanes_lo[8]), lanes_lo[9])), XOR(lanes_lo[8], AND(NOT(lanes_lo[9]), lanes_lo[10])) + lanes_lo[11], lanes_lo[12], lanes_lo[13], lanes_lo[14], lanes_lo[15] = XOR(lanes_lo[12], AND(NOT(lanes_lo[13]), lanes_lo[14])), XOR(lanes_lo[13], AND(NOT(lanes_lo[14]), lanes_lo[15])), XOR(lanes_lo[14], AND(NOT(lanes_lo[15]), lanes_lo[11])), XOR(lanes_lo[15], AND(NOT(lanes_lo[11]), lanes_lo[12])), XOR(lanes_lo[11], AND(NOT(lanes_lo[12]), lanes_lo[13])) + lanes_lo[16], lanes_lo[17], lanes_lo[18], lanes_lo[19], lanes_lo[20] = XOR(lanes_lo[20], AND(NOT(lanes_lo[16]), lanes_lo[17])), XOR(lanes_lo[16], AND(NOT(lanes_lo[17]), lanes_lo[18])), XOR(lanes_lo[17], AND(NOT(lanes_lo[18]), lanes_lo[19])), XOR(lanes_lo[18], AND(NOT(lanes_lo[19]), lanes_lo[20])), XOR(lanes_lo[19], AND(NOT(lanes_lo[20]), lanes_lo[16])) + lanes_lo[21], lanes_lo[22], lanes_lo[23], lanes_lo[24], lanes_lo[25] = XOR(lanes_lo[23], AND(NOT(lanes_lo[24]), lanes_lo[25])), XOR(lanes_lo[24], AND(NOT(lanes_lo[25]), lanes_lo[21])), XOR(lanes_lo[25], AND(NOT(lanes_lo[21]), lanes_lo[22])), XOR(lanes_lo[21], AND(NOT(lanes_lo[22]), lanes_lo[23])), XOR(lanes_lo[22], AND(NOT(lanes_lo[23]), lanes_lo[24])) + lanes_hi[1], lanes_hi[2], lanes_hi[3], lanes_hi[4], lanes_hi[5] = XOR(lanes_hi[1], AND(NOT(lanes_hi[2]), lanes_hi[3]), RC_hi[round_idx]), XOR(lanes_hi[2], AND(NOT(lanes_hi[3]), lanes_hi[4])), XOR(lanes_hi[3], AND(NOT(lanes_hi[4]), lanes_hi[5])), XOR(lanes_hi[4], AND(NOT(lanes_hi[5]), lanes_hi[1])), XOR(lanes_hi[5], AND(NOT(lanes_hi[1]), lanes_hi[2])) + lanes_hi[6], lanes_hi[7], lanes_hi[8], lanes_hi[9], lanes_hi[10] = XOR(lanes_hi[9], AND(NOT(lanes_hi[10]), lanes_hi[6])), XOR(lanes_hi[10], AND(NOT(lanes_hi[6]), lanes_hi[7])), XOR(lanes_hi[6], AND(NOT(lanes_hi[7]), lanes_hi[8])), XOR(lanes_hi[7], AND(NOT(lanes_hi[8]), lanes_hi[9])), XOR(lanes_hi[8], AND(NOT(lanes_hi[9]), lanes_hi[10])) + lanes_hi[11], lanes_hi[12], lanes_hi[13], lanes_hi[14], lanes_hi[15] = XOR(lanes_hi[12], AND(NOT(lanes_hi[13]), lanes_hi[14])), XOR(lanes_hi[13], AND(NOT(lanes_hi[14]), lanes_hi[15])), XOR(lanes_hi[14], AND(NOT(lanes_hi[15]), lanes_hi[11])), XOR(lanes_hi[15], AND(NOT(lanes_hi[11]), lanes_hi[12])), XOR(lanes_hi[11], AND(NOT(lanes_hi[12]), lanes_hi[13])) + lanes_hi[16], lanes_hi[17], lanes_hi[18], lanes_hi[19], lanes_hi[20] = XOR(lanes_hi[20], AND(NOT(lanes_hi[16]), lanes_hi[17])), XOR(lanes_hi[16], AND(NOT(lanes_hi[17]), lanes_hi[18])), XOR(lanes_hi[17], AND(NOT(lanes_hi[18]), lanes_hi[19])), XOR(lanes_hi[18], AND(NOT(lanes_hi[19]), lanes_hi[20])), XOR(lanes_hi[19], AND(NOT(lanes_hi[20]), lanes_hi[16])) + lanes_hi[21], lanes_hi[22], lanes_hi[23], lanes_hi[24], lanes_hi[25] = XOR(lanes_hi[23], AND(NOT(lanes_hi[24]), lanes_hi[25])), XOR(lanes_hi[24], AND(NOT(lanes_hi[25]), lanes_hi[21])), XOR(lanes_hi[25], AND(NOT(lanes_hi[21]), lanes_hi[22])), XOR(lanes_hi[21], AND(NOT(lanes_hi[22]), lanes_hi[23])), XOR(lanes_hi[22], AND(NOT(lanes_hi[23]), lanes_hi[24])) + end + end + end + +end + + +if branch == "LJ" then + + + -- SHA256 implementation for "LuaJIT without FFI" branch + + function sha256_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W, sha2_K_hi + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for j = 17, 64 do + local a, b = W[j-15], W[j-2] + W[j] = NORM( NORM( XOR(ROR(a, 7), ROL(a, 14), SHR(a, 3)) + XOR(ROL(b, 15), ROL(b, 13), SHR(b, 10)) ) + NORM( W[j-7] + W[j-16] ) ) + end + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for j = 1, 64, 8 do -- Thanks to Peter Cawley for this workaround (unroll the loop to avoid "PHI shuffling too complex" due to PHIs overlap) + local z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j] + W[j] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+1] + W[j+1] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+2] + W[j+2] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+3] + W[j+3] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+4] + W[j+4] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+5] + W[j+5] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+6] + W[j+6] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+7] + W[j+7] + h) ) + h, g, f, e = g, f, e, NORM(d + z) + d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z ) + end + H[1], H[2], H[3], H[4] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]) + H[5], H[6], H[7], H[8] = NORM(e + H[5]), NORM(f + H[6]), NORM(g + H[7]), NORM(h + H[8]) + end + end + + local function ADD64_4(a_lo, a_hi, b_lo, b_hi, c_lo, c_hi, d_lo, d_hi) + local sum_lo = a_lo % 2^32 + b_lo % 2^32 + c_lo % 2^32 + d_lo % 2^32 + local sum_hi = a_hi + b_hi + c_hi + d_hi + local result_lo = NORM( sum_lo ) + local result_hi = NORM( sum_hi + floor(sum_lo / 2^32) ) + return result_lo, result_hi + end + + if LuaJIT_arch == "x86" then -- Special trick is required to avoid "PHI shuffling too complex" on x86 platform + + + -- SHA512 implementation for "LuaJIT x86 without FFI" branch + + function sha512_feed_128(H_lo, H_hi, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + -- W1_hi, W1_lo, W2_hi, W2_lo, ... Wk_hi = W[2*k-1], Wk_lo = W[2*k] + local W, K_lo, K_hi = common_W, sha2_K_lo, sha2_K_hi + for pos = offs, offs + size - 1, 128 do + for j = 1, 16*2 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for jj = 17*2, 80*2, 2 do + local a_lo, a_hi = W[jj-30], W[jj-31] + local t_lo = XOR(OR(SHR(a_lo, 1), SHL(a_hi, 31)), OR(SHR(a_lo, 8), SHL(a_hi, 24)), OR(SHR(a_lo, 7), SHL(a_hi, 25))) + local t_hi = XOR(OR(SHR(a_hi, 1), SHL(a_lo, 31)), OR(SHR(a_hi, 8), SHL(a_lo, 24)), SHR(a_hi, 7)) + local b_lo, b_hi = W[jj-4], W[jj-5] + local u_lo = XOR(OR(SHR(b_lo, 19), SHL(b_hi, 13)), OR(SHL(b_lo, 3), SHR(b_hi, 29)), OR(SHR(b_lo, 6), SHL(b_hi, 26))) + local u_hi = XOR(OR(SHR(b_hi, 19), SHL(b_lo, 13)), OR(SHL(b_hi, 3), SHR(b_lo, 29)), SHR(b_hi, 6)) + W[jj], W[jj-1] = ADD64_4(t_lo, t_hi, u_lo, u_hi, W[jj-14], W[jj-15], W[jj-32], W[jj-33]) + end + local a_lo, b_lo, c_lo, d_lo, e_lo, f_lo, g_lo, h_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local a_hi, b_hi, c_hi, d_hi, e_hi, f_hi, g_hi, h_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + local zero = 0 + for j = 1, 80 do + local t_lo = XOR(g_lo, AND(e_lo, XOR(f_lo, g_lo))) + local t_hi = XOR(g_hi, AND(e_hi, XOR(f_hi, g_hi))) + local u_lo = XOR(OR(SHR(e_lo, 14), SHL(e_hi, 18)), OR(SHR(e_lo, 18), SHL(e_hi, 14)), OR(SHL(e_lo, 23), SHR(e_hi, 9))) + local u_hi = XOR(OR(SHR(e_hi, 14), SHL(e_lo, 18)), OR(SHR(e_hi, 18), SHL(e_lo, 14)), OR(SHL(e_hi, 23), SHR(e_lo, 9))) + local sum_lo = u_lo % 2^32 + t_lo % 2^32 + h_lo % 2^32 + K_lo[j] + W[2*j] % 2^32 + local z_lo, z_hi = NORM( sum_lo ), NORM( u_hi + t_hi + h_hi + K_hi[j] + W[2*j-1] + floor(sum_lo / 2^32) ) + zero = zero + zero -- this thick is needed to avoid "PHI shuffling too complex" due to PHIs overlap + h_lo, h_hi, g_lo, g_hi, f_lo, f_hi = OR(zero, g_lo), OR(zero, g_hi), OR(zero, f_lo), OR(zero, f_hi), OR(zero, e_lo), OR(zero, e_hi) + local sum_lo = z_lo % 2^32 + d_lo % 2^32 + e_lo, e_hi = NORM( sum_lo ), NORM( z_hi + d_hi + floor(sum_lo / 2^32) ) + d_lo, d_hi, c_lo, c_hi, b_lo, b_hi = OR(zero, c_lo), OR(zero, c_hi), OR(zero, b_lo), OR(zero, b_hi), OR(zero, a_lo), OR(zero, a_hi) + u_lo = XOR(OR(SHR(b_lo, 28), SHL(b_hi, 4)), OR(SHL(b_lo, 30), SHR(b_hi, 2)), OR(SHL(b_lo, 25), SHR(b_hi, 7))) + u_hi = XOR(OR(SHR(b_hi, 28), SHL(b_lo, 4)), OR(SHL(b_hi, 30), SHR(b_lo, 2)), OR(SHL(b_hi, 25), SHR(b_lo, 7))) + t_lo = OR(AND(d_lo, c_lo), AND(b_lo, XOR(d_lo, c_lo))) + t_hi = OR(AND(d_hi, c_hi), AND(b_hi, XOR(d_hi, c_hi))) + local sum_lo = z_lo % 2^32 + t_lo % 2^32 + u_lo % 2^32 + a_lo, a_hi = NORM( sum_lo ), NORM( z_hi + t_hi + u_hi + floor(sum_lo / 2^32) ) + end + H_lo[1], H_hi[1] = ADD64_4(H_lo[1], H_hi[1], a_lo, a_hi, 0, 0, 0, 0) + H_lo[2], H_hi[2] = ADD64_4(H_lo[2], H_hi[2], b_lo, b_hi, 0, 0, 0, 0) + H_lo[3], H_hi[3] = ADD64_4(H_lo[3], H_hi[3], c_lo, c_hi, 0, 0, 0, 0) + H_lo[4], H_hi[4] = ADD64_4(H_lo[4], H_hi[4], d_lo, d_hi, 0, 0, 0, 0) + H_lo[5], H_hi[5] = ADD64_4(H_lo[5], H_hi[5], e_lo, e_hi, 0, 0, 0, 0) + H_lo[6], H_hi[6] = ADD64_4(H_lo[6], H_hi[6], f_lo, f_hi, 0, 0, 0, 0) + H_lo[7], H_hi[7] = ADD64_4(H_lo[7], H_hi[7], g_lo, g_hi, 0, 0, 0, 0) + H_lo[8], H_hi[8] = ADD64_4(H_lo[8], H_hi[8], h_lo, h_hi, 0, 0, 0, 0) + end + end + + else -- all platforms except x86 + + + -- SHA512 implementation for "LuaJIT non-x86 without FFI" branch + + function sha512_feed_128(H_lo, H_hi, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + -- W1_hi, W1_lo, W2_hi, W2_lo, ... Wk_hi = W[2*k-1], Wk_lo = W[2*k] + local W, K_lo, K_hi = common_W, sha2_K_lo, sha2_K_hi + for pos = offs, offs + size - 1, 128 do + for j = 1, 16*2 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for jj = 17*2, 80*2, 2 do + local a_lo, a_hi = W[jj-30], W[jj-31] + local t_lo = XOR(OR(SHR(a_lo, 1), SHL(a_hi, 31)), OR(SHR(a_lo, 8), SHL(a_hi, 24)), OR(SHR(a_lo, 7), SHL(a_hi, 25))) + local t_hi = XOR(OR(SHR(a_hi, 1), SHL(a_lo, 31)), OR(SHR(a_hi, 8), SHL(a_lo, 24)), SHR(a_hi, 7)) + local b_lo, b_hi = W[jj-4], W[jj-5] + local u_lo = XOR(OR(SHR(b_lo, 19), SHL(b_hi, 13)), OR(SHL(b_lo, 3), SHR(b_hi, 29)), OR(SHR(b_lo, 6), SHL(b_hi, 26))) + local u_hi = XOR(OR(SHR(b_hi, 19), SHL(b_lo, 13)), OR(SHL(b_hi, 3), SHR(b_lo, 29)), SHR(b_hi, 6)) + W[jj], W[jj-1] = ADD64_4(t_lo, t_hi, u_lo, u_hi, W[jj-14], W[jj-15], W[jj-32], W[jj-33]) + end + local a_lo, b_lo, c_lo, d_lo, e_lo, f_lo, g_lo, h_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local a_hi, b_hi, c_hi, d_hi, e_hi, f_hi, g_hi, h_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for j = 1, 80 do + local t_lo = XOR(g_lo, AND(e_lo, XOR(f_lo, g_lo))) + local t_hi = XOR(g_hi, AND(e_hi, XOR(f_hi, g_hi))) + local u_lo = XOR(OR(SHR(e_lo, 14), SHL(e_hi, 18)), OR(SHR(e_lo, 18), SHL(e_hi, 14)), OR(SHL(e_lo, 23), SHR(e_hi, 9))) + local u_hi = XOR(OR(SHR(e_hi, 14), SHL(e_lo, 18)), OR(SHR(e_hi, 18), SHL(e_lo, 14)), OR(SHL(e_hi, 23), SHR(e_lo, 9))) + local sum_lo = u_lo % 2^32 + t_lo % 2^32 + h_lo % 2^32 + K_lo[j] + W[2*j] % 2^32 + local z_lo, z_hi = NORM( sum_lo ), NORM( u_hi + t_hi + h_hi + K_hi[j] + W[2*j-1] + floor(sum_lo / 2^32) ) + h_lo, h_hi, g_lo, g_hi, f_lo, f_hi = g_lo, g_hi, f_lo, f_hi, e_lo, e_hi + local sum_lo = z_lo % 2^32 + d_lo % 2^32 + e_lo, e_hi = NORM( sum_lo ), NORM( z_hi + d_hi + floor(sum_lo / 2^32) ) + d_lo, d_hi, c_lo, c_hi, b_lo, b_hi = c_lo, c_hi, b_lo, b_hi, a_lo, a_hi + u_lo = XOR(OR(SHR(b_lo, 28), SHL(b_hi, 4)), OR(SHL(b_lo, 30), SHR(b_hi, 2)), OR(SHL(b_lo, 25), SHR(b_hi, 7))) + u_hi = XOR(OR(SHR(b_hi, 28), SHL(b_lo, 4)), OR(SHL(b_hi, 30), SHR(b_lo, 2)), OR(SHL(b_hi, 25), SHR(b_lo, 7))) + t_lo = OR(AND(d_lo, c_lo), AND(b_lo, XOR(d_lo, c_lo))) + t_hi = OR(AND(d_hi, c_hi), AND(b_hi, XOR(d_hi, c_hi))) + local sum_lo = z_lo % 2^32 + u_lo % 2^32 + t_lo % 2^32 + a_lo, a_hi = NORM( sum_lo ), NORM( z_hi + u_hi + t_hi + floor(sum_lo / 2^32) ) + end + H_lo[1], H_hi[1] = ADD64_4(H_lo[1], H_hi[1], a_lo, a_hi, 0, 0, 0, 0) + H_lo[2], H_hi[2] = ADD64_4(H_lo[2], H_hi[2], b_lo, b_hi, 0, 0, 0, 0) + H_lo[3], H_hi[3] = ADD64_4(H_lo[3], H_hi[3], c_lo, c_hi, 0, 0, 0, 0) + H_lo[4], H_hi[4] = ADD64_4(H_lo[4], H_hi[4], d_lo, d_hi, 0, 0, 0, 0) + H_lo[5], H_hi[5] = ADD64_4(H_lo[5], H_hi[5], e_lo, e_hi, 0, 0, 0, 0) + H_lo[6], H_hi[6] = ADD64_4(H_lo[6], H_hi[6], f_lo, f_hi, 0, 0, 0, 0) + H_lo[7], H_hi[7] = ADD64_4(H_lo[7], H_hi[7], g_lo, g_hi, 0, 0, 0, 0) + H_lo[8], H_hi[8] = ADD64_4(H_lo[8], H_hi[8], h_lo, h_hi, 0, 0, 0, 0) + end + end + + end + + + -- MD5 implementation for "LuaJIT without FFI" branch + + function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W, md5_K + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a) + end + local a, b, c, d = H[1], H[2], H[3], H[4] + for j = 1, 16, 4 do + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j ] + W[j ] + a), 7) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+1] + W[j+1] + a), 12) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+2] + W[j+2] + a), 17) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(d, AND(b, XOR(c, d))) + (K[j+3] + W[j+3] + a), 22) + b) + end + for j = 17, 32, 4 do + local g = 5*j-4 + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j ] + W[AND(g , 15) + 1] + a), 5) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+1] + W[AND(g + 5, 15) + 1] + a), 9) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+2] + W[AND(g + 10, 15) + 1] + a), 14) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, AND(d, XOR(b, c))) + (K[j+3] + W[AND(g - 1, 15) + 1] + a), 20) + b) + end + for j = 33, 48, 4 do + local g = 3*j+2 + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j ] + W[AND(g , 15) + 1] + a), 4) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+1] + W[AND(g + 3, 15) + 1] + a), 11) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+2] + W[AND(g + 6, 15) + 1] + a), 16) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(b, c, d) + (K[j+3] + W[AND(g - 7, 15) + 1] + a), 23) + b) + end + for j = 49, 64, 4 do + local g = j*7 + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j ] + W[AND(g - 7, 15) + 1] + a), 6) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+1] + W[AND(g , 15) + 1] + a), 10) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+2] + W[AND(g + 7, 15) + 1] + a), 15) + b) + a, d, c, b = d, c, b, NORM(ROL(XOR(c, OR(b, NOT(d))) + (K[j+3] + W[AND(g - 2, 15) + 1] + a), 21) + b) + end + H[1], H[2], H[3], H[4] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]) + end + end + + + -- SHA-1 implementation for "LuaJIT without FFI" branch + + function sha1_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d) + end + for j = 17, 80 do + W[j] = ROL(XOR(W[j-3], W[j-8], W[j-14], W[j-16]), 1) + end + local a, b, c, d, e = H[1], H[2], H[3], H[4], H[5] + for j = 1, 20, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j] + 0x5A827999 + e)) -- constant = floor(2^30 * sqrt(2)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+1] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+2] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+3] + 0x5A827999 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(d, AND(b, XOR(d, c))) + (W[j+4] + 0x5A827999 + e)) + end + for j = 21, 40, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j] + 0x6ED9EBA1 + e)) -- 2^30 * sqrt(3) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+1] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+2] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+3] + 0x6ED9EBA1 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+4] + 0x6ED9EBA1 + e)) + end + for j = 41, 60, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j] + 0x8F1BBCDC + e)) -- 2^30 * sqrt(5) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+1] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+2] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+3] + 0x8F1BBCDC + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(AND(d, XOR(b, c)), AND(b, c)) + (W[j+4] + 0x8F1BBCDC + e)) + end + for j = 61, 80, 5 do + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j] + 0xCA62C1D6 + e)) -- 2^30 * sqrt(10) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+1] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+2] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+3] + 0xCA62C1D6 + e)) + e, d, c, b, a = d, c, ROR(b, 2), a, NORM(ROL(a, 5) + XOR(b, c, d) + (W[j+4] + 0xCA62C1D6 + e)) + end + H[1], H[2], H[3], H[4], H[5] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4]), NORM(e + H[5]) + end + end + + + -- BLAKE2b implementation for "LuaJIT without FFI" branch + + do + local v_lo, v_hi = {}, {} + + local function G(a, b, c, d, k1, k2) + local W = common_W + local va_lo, vb_lo, vc_lo, vd_lo = v_lo[a], v_lo[b], v_lo[c], v_lo[d] + local va_hi, vb_hi, vc_hi, vd_hi = v_hi[a], v_hi[b], v_hi[c], v_hi[d] + local z = W[2*k1-1] + (va_lo % 2^32 + vb_lo % 2^32) + va_lo = NORM(z) + va_hi = NORM(W[2*k1] + (va_hi + vb_hi + floor(z / 2^32))) + vd_lo, vd_hi = XOR(vd_hi, va_hi), XOR(vd_lo, va_lo) + z = vc_lo % 2^32 + vd_lo % 2^32 + vc_lo = NORM(z) + vc_hi = NORM(vc_hi + vd_hi + floor(z / 2^32)) + vb_lo, vb_hi = XOR(vb_lo, vc_lo), XOR(vb_hi, vc_hi) + vb_lo, vb_hi = XOR(SHR(vb_lo, 24), SHL(vb_hi, 8)), XOR(SHR(vb_hi, 24), SHL(vb_lo, 8)) + z = W[2*k2-1] + (va_lo % 2^32 + vb_lo % 2^32) + va_lo = NORM(z) + va_hi = NORM(W[2*k2] + (va_hi + vb_hi + floor(z / 2^32))) + vd_lo, vd_hi = XOR(vd_lo, va_lo), XOR(vd_hi, va_hi) + vd_lo, vd_hi = XOR(SHR(vd_lo, 16), SHL(vd_hi, 16)), XOR(SHR(vd_hi, 16), SHL(vd_lo, 16)) + z = vc_lo % 2^32 + vd_lo % 2^32 + vc_lo = NORM(z) + vc_hi = NORM(vc_hi + vd_hi + floor(z / 2^32)) + vb_lo, vb_hi = XOR(vb_lo, vc_lo), XOR(vb_hi, vc_hi) + vb_lo, vb_hi = XOR(SHL(vb_lo, 1), SHR(vb_hi, 31)), XOR(SHL(vb_hi, 1), SHR(vb_lo, 31)) + v_lo[a], v_lo[b], v_lo[c], v_lo[d] = va_lo, vb_lo, vc_lo, vd_lo + v_hi[a], v_hi[b], v_hi[c], v_hi[d] = va_hi, vb_hi, vc_hi, vd_hi + end + + function blake2b_feed_128(H_lo, H_hi, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local W = common_W + local h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for pos = offs, offs + size - 1, 128 do + if str then + for j = 1, 32 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = d * 2^24 + OR(SHL(c, 16), SHL(b, 8), a) + end + end + v_lo[0x0], v_lo[0x1], v_lo[0x2], v_lo[0x3], v_lo[0x4], v_lo[0x5], v_lo[0x6], v_lo[0x7] = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + v_lo[0x8], v_lo[0x9], v_lo[0xA], v_lo[0xB], v_lo[0xC], v_lo[0xD], v_lo[0xE], v_lo[0xF] = sha2_H_lo[1], sha2_H_lo[2], sha2_H_lo[3], sha2_H_lo[4], sha2_H_lo[5], sha2_H_lo[6], sha2_H_lo[7], sha2_H_lo[8] + v_hi[0x0], v_hi[0x1], v_hi[0x2], v_hi[0x3], v_hi[0x4], v_hi[0x5], v_hi[0x6], v_hi[0x7] = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + v_hi[0x8], v_hi[0x9], v_hi[0xA], v_hi[0xB], v_hi[0xC], v_hi[0xD], v_hi[0xE], v_hi[0xF] = sha2_H_hi[1], sha2_H_hi[2], sha2_H_hi[3], sha2_H_hi[4], sha2_H_hi[5], sha2_H_hi[6], sha2_H_hi[7], sha2_H_hi[8] + bytes_compressed = bytes_compressed + (last_block_size or 128) + local t0_lo = bytes_compressed % 2^32 + local t0_hi = floor(bytes_compressed / 2^32) + v_lo[0xC] = XOR(v_lo[0xC], t0_lo) -- t0 = low_8_bytes(bytes_compressed) + v_hi[0xC] = XOR(v_hi[0xC], t0_hi) + -- t1 = high_8_bytes(bytes_compressed) = 0, message length is always below 2^53 bytes + if last_block_size then -- flag f0 + v_lo[0xE] = NOT(v_lo[0xE]) + v_hi[0xE] = NOT(v_hi[0xE]) + end + if is_last_node then -- flag f1 + v_lo[0xF] = NOT(v_lo[0xF]) + v_hi[0xF] = NOT(v_hi[0xF]) + end + for j = 1, 12 do + local row = sigma[j] + G(0, 4, 8, 12, row[ 1], row[ 2]) + G(1, 5, 9, 13, row[ 3], row[ 4]) + G(2, 6, 10, 14, row[ 5], row[ 6]) + G(3, 7, 11, 15, row[ 7], row[ 8]) + G(0, 5, 10, 15, row[ 9], row[10]) + G(1, 6, 11, 12, row[11], row[12]) + G(2, 7, 8, 13, row[13], row[14]) + G(3, 4, 9, 14, row[15], row[16]) + end + h1_lo = XOR(h1_lo, v_lo[0x0], v_lo[0x8]) + h2_lo = XOR(h2_lo, v_lo[0x1], v_lo[0x9]) + h3_lo = XOR(h3_lo, v_lo[0x2], v_lo[0xA]) + h4_lo = XOR(h4_lo, v_lo[0x3], v_lo[0xB]) + h5_lo = XOR(h5_lo, v_lo[0x4], v_lo[0xC]) + h6_lo = XOR(h6_lo, v_lo[0x5], v_lo[0xD]) + h7_lo = XOR(h7_lo, v_lo[0x6], v_lo[0xE]) + h8_lo = XOR(h8_lo, v_lo[0x7], v_lo[0xF]) + h1_hi = XOR(h1_hi, v_hi[0x0], v_hi[0x8]) + h2_hi = XOR(h2_hi, v_hi[0x1], v_hi[0x9]) + h3_hi = XOR(h3_hi, v_hi[0x2], v_hi[0xA]) + h4_hi = XOR(h4_hi, v_hi[0x3], v_hi[0xB]) + h5_hi = XOR(h5_hi, v_hi[0x4], v_hi[0xC]) + h6_hi = XOR(h6_hi, v_hi[0x5], v_hi[0xD]) + h7_hi = XOR(h7_hi, v_hi[0x6], v_hi[0xE]) + h8_hi = XOR(h8_hi, v_hi[0x7], v_hi[0xF]) + end + H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] = h1_lo % 2^32, h2_lo % 2^32, h3_lo % 2^32, h4_lo % 2^32, h5_lo % 2^32, h6_lo % 2^32, h7_lo % 2^32, h8_lo % 2^32 + H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] = h1_hi % 2^32, h2_hi % 2^32, h3_hi % 2^32, h4_hi % 2^32, h5_hi % 2^32, h6_hi % 2^32, h7_hi % 2^32, h8_hi % 2^32 + return bytes_compressed + end + + end +end + + +if branch == "FFI" or branch == "LJ" then + + + -- BLAKE2s and BLAKE3 implementations for "LuaJIT with FFI" and "LuaJIT without FFI" branches + + do + local W = common_W_blake2s + local v = v_for_blake2s_feed_64 + + local function G(a, b, c, d, k1, k2) + local va, vb, vc, vd = v[a], v[b], v[c], v[d] + va = NORM(W[k1] + (va + vb)) + vd = ROR(XOR(vd, va), 16) + vc = NORM(vc + vd) + vb = ROR(XOR(vb, vc), 12) + va = NORM(W[k2] + (va + vb)) + vd = ROR(XOR(vd, va), 8) + vc = NORM(vc + vd) + vb = ROR(XOR(vb, vc), 7) + v[a], v[b], v[c], v[d] = va, vb, vc, vd + end + + function blake2s_feed_64(H, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 64 + local h1, h2, h3, h4, h5, h6, h7, h8 = NORM(H[1]), NORM(H[2]), NORM(H[3]), NORM(H[4]), NORM(H[5]), NORM(H[6]), NORM(H[7]), NORM(H[8]) + for pos = offs, offs + size - 1, 64 do + if str then + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a) + end + end + v[0x0], v[0x1], v[0x2], v[0x3], v[0x4], v[0x5], v[0x6], v[0x7] = h1, h2, h3, h4, h5, h6, h7, h8 + v[0x8], v[0x9], v[0xA], v[0xB], v[0xE], v[0xF] = NORM(sha2_H_hi[1]), NORM(sha2_H_hi[2]), NORM(sha2_H_hi[3]), NORM(sha2_H_hi[4]), NORM(sha2_H_hi[7]), NORM(sha2_H_hi[8]) + bytes_compressed = bytes_compressed + (last_block_size or 64) + local t0 = bytes_compressed % 2^32 + local t1 = floor(bytes_compressed / 2^32) + v[0xC] = XOR(sha2_H_hi[5], t0) -- t0 = low_4_bytes(bytes_compressed) + v[0xD] = XOR(sha2_H_hi[6], t1) -- t1 = high_4_bytes(bytes_compressed + if last_block_size then -- flag f0 + v[0xE] = NOT(v[0xE]) + end + if is_last_node then -- flag f1 + v[0xF] = NOT(v[0xF]) + end + for j = 1, 10 do + local row = sigma[j] + G(0, 4, 8, 12, row[ 1], row[ 2]) + G(1, 5, 9, 13, row[ 3], row[ 4]) + G(2, 6, 10, 14, row[ 5], row[ 6]) + G(3, 7, 11, 15, row[ 7], row[ 8]) + G(0, 5, 10, 15, row[ 9], row[10]) + G(1, 6, 11, 12, row[11], row[12]) + G(2, 7, 8, 13, row[13], row[14]) + G(3, 4, 9, 14, row[15], row[16]) + end + h1 = XOR(h1, v[0x0], v[0x8]) + h2 = XOR(h2, v[0x1], v[0x9]) + h3 = XOR(h3, v[0x2], v[0xA]) + h4 = XOR(h4, v[0x3], v[0xB]) + h5 = XOR(h5, v[0x4], v[0xC]) + h6 = XOR(h6, v[0x5], v[0xD]) + h7 = XOR(h7, v[0x6], v[0xE]) + h8 = XOR(h8, v[0x7], v[0xF]) + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + function blake3_feed_64(str, offs, size, flags, chunk_index, H_in, H_out, wide_output, block_length) + -- offs >= 0, size >= 0, size is multiple of 64 + block_length = block_length or 64 + local h1, h2, h3, h4, h5, h6, h7, h8 = NORM(H_in[1]), NORM(H_in[2]), NORM(H_in[3]), NORM(H_in[4]), NORM(H_in[5]), NORM(H_in[6]), NORM(H_in[7]), NORM(H_in[8]) + H_out = H_out or H_in + for pos = offs, offs + size - 1, 64 do + if str then + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a) + end + end + v[0x0], v[0x1], v[0x2], v[0x3], v[0x4], v[0x5], v[0x6], v[0x7] = h1, h2, h3, h4, h5, h6, h7, h8 + v[0x8], v[0x9], v[0xA], v[0xB] = NORM(sha2_H_hi[1]), NORM(sha2_H_hi[2]), NORM(sha2_H_hi[3]), NORM(sha2_H_hi[4]) + v[0xC] = NORM(chunk_index % 2^32) -- t0 = low_4_bytes(chunk_index) + v[0xD] = floor(chunk_index / 2^32) -- t1 = high_4_bytes(chunk_index) + v[0xE], v[0xF] = block_length, flags + for j = 1, 7 do + G(0, 4, 8, 12, perm_blake3[j], perm_blake3[j + 14]) + G(1, 5, 9, 13, perm_blake3[j + 1], perm_blake3[j + 2]) + G(2, 6, 10, 14, perm_blake3[j + 16], perm_blake3[j + 7]) + G(3, 7, 11, 15, perm_blake3[j + 15], perm_blake3[j + 17]) + G(0, 5, 10, 15, perm_blake3[j + 21], perm_blake3[j + 5]) + G(1, 6, 11, 12, perm_blake3[j + 3], perm_blake3[j + 6]) + G(2, 7, 8, 13, perm_blake3[j + 4], perm_blake3[j + 18]) + G(3, 4, 9, 14, perm_blake3[j + 19], perm_blake3[j + 20]) + end + if wide_output then + H_out[ 9] = XOR(h1, v[0x8]) + H_out[10] = XOR(h2, v[0x9]) + H_out[11] = XOR(h3, v[0xA]) + H_out[12] = XOR(h4, v[0xB]) + H_out[13] = XOR(h5, v[0xC]) + H_out[14] = XOR(h6, v[0xD]) + H_out[15] = XOR(h7, v[0xE]) + H_out[16] = XOR(h8, v[0xF]) + end + h1 = XOR(v[0x0], v[0x8]) + h2 = XOR(v[0x1], v[0x9]) + h3 = XOR(v[0x2], v[0xA]) + h4 = XOR(v[0x3], v[0xB]) + h5 = XOR(v[0x4], v[0xC]) + h6 = XOR(v[0x5], v[0xD]) + h7 = XOR(v[0x6], v[0xE]) + h8 = XOR(v[0x7], v[0xF]) + end + H_out[1], H_out[2], H_out[3], H_out[4], H_out[5], H_out[6], H_out[7], H_out[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + end + +end + + +if branch == "INT64" then + + + -- implementation for Lua 5.3/5.4 + + hi_factor = 4294967296 + hi_factor_keccak = 4294967296 + lanes_index_base = 1 + + HEX64, XORA5, XOR_BYTE, sha256_feed_64, sha512_feed_128, md5_feed_64, sha1_feed_64, keccak_feed, blake2s_feed_64, blake2b_feed_128, blake3_feed_64 = load[=[-- branch "INT64" + local md5_next_shift, md5_K, sha2_K_lo, sha2_K_hi, build_keccak_format, sha3_RC_lo, sigma, common_W, sha2_H_lo, sha2_H_hi, perm_blake3 = ... + local string_format, string_unpack = string.format, string.unpack + + local function HEX64(x) + return string_format("%016x", x) + end + + local function XORA5(x, y) + return x ~ (y or 0xa5a5a5a5a5a5a5a5) + end + + local function XOR_BYTE(x, y) + return x ~ y + end + + local function sha256_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W, sha2_K_hi + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4", str, pos) + for j = 17, 64 do + local a = W[j-15] + a = a<<32 | a + local b = W[j-2] + b = b<<32 | b + W[j] = (a>>7 ~ a>>18 ~ a>>35) + (b>>17 ~ b>>19 ~ b>>42) + W[j-7] + W[j-16] & (1<<32)-1 + end + local a, b, c, d, e, f, g, h = h1, h2, h3, h4, h5, h6, h7, h8 + for j = 1, 64 do + e = e<<32 | e & (1<<32)-1 + local z = (e>>6 ~ e>>11 ~ e>>25) + (g ~ e & (f ~ g)) + h + K[j] + W[j] + h = g + g = f + f = e + e = z + d + d = c + c = b + b = a + a = a<<32 | a & (1<<32)-1 + a = z + ((a ~ c) & d ~ a & c) + (a>>2 ~ a>>13 ~ a>>22) + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + h5 = e + h5 + h6 = f + h6 + h7 = g + h7 + h8 = h + h8 + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + local function sha512_feed_128(H, _, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + local W, K = common_W, sha2_K_lo + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 128 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">i8i8i8i8i8i8i8i8i8i8i8i8i8i8i8i8", str, pos) + for j = 17, 80 do + local a = W[j-15] + local b = W[j-2] + W[j] = (a >> 1 ~ a >> 7 ~ a >> 8 ~ a << 56 ~ a << 63) + (b >> 6 ~ b >> 19 ~ b >> 61 ~ b << 3 ~ b << 45) + W[j-7] + W[j-16] + end + local a, b, c, d, e, f, g, h = h1, h2, h3, h4, h5, h6, h7, h8 + for j = 1, 80 do + local z = (e >> 14 ~ e >> 18 ~ e >> 41 ~ e << 23 ~ e << 46 ~ e << 50) + (g ~ e & (f ~ g)) + h + K[j] + W[j] + h = g + g = f + f = e + e = z + d + d = c + c = b + b = a + a = z + ((a ~ c) & d ~ a & c) + (a >> 28 ~ a >> 34 ~ a >> 39 ~ a << 25 ~ a << 30 ~ a << 36) + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + h5 = e + h5 + h6 = f + h6 + h7 = g + h7 + h8 = h + h8 + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + local function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K, md5_next_shift = common_W, md5_K, md5_next_shift + local h1, h2, h3, h4 = H[1], H[2], H[3], H[4] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> s) + b + s = md5_next_shift[s] + end + s = 32-5 + for j = 17, 32 do + local F = (c ~ d & (b ~ c)) + a + K[j] + W[(5*j-4 & 15) + 1] + a = d + d = c + c = b + b = ((F<<32 | F & (1<<32)-1) >> s) + b + s = md5_next_shift[s] + end + s = 32-4 + for j = 33, 48 do + local F = (b ~ c ~ d) + a + K[j] + W[(3*j+2 & 15) + 1] + a = d + d = c + c = b + b = ((F<<32 | F & (1<<32)-1) >> s) + b + s = md5_next_shift[s] + end + s = 32-6 + for j = 49, 64 do + local F = (c ~ (b | ~d)) + a + K[j] + W[(j*7-7 & 15) + 1] + a = d + d = c + c = b + b = ((F<<32 | F & (1<<32)-1) >> s) + b + s = md5_next_shift[s] + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + end + H[1], H[2], H[3], H[4] = h1, h2, h3, h4 + end + + local function sha1_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5 = H[1], H[2], H[3], H[4], H[5] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4", str, pos) + for j = 17, 80 do + local a = W[j-3] ~ W[j-8] ~ W[j-14] ~ W[j-16] + W[j] = (a<<32 | a) << 1 >> 32 + end + local a, b, c, d, e = h1, h2, h3, h4, h5 + for j = 1, 20 do + local z = ((a<<32 | a & (1<<32)-1) >> 27) + (d ~ b & (c ~ d)) + 0x5A827999 + W[j] + e -- constant = floor(2^30 * sqrt(2)) + e = d + d = c + c = (b<<32 | b & (1<<32)-1) >> 2 + b = a + a = z + end + for j = 21, 40 do + local z = ((a<<32 | a & (1<<32)-1) >> 27) + (b ~ c ~ d) + 0x6ED9EBA1 + W[j] + e -- 2^30 * sqrt(3) + e = d + d = c + c = (b<<32 | b & (1<<32)-1) >> 2 + b = a + a = z + end + for j = 41, 60 do + local z = ((a<<32 | a & (1<<32)-1) >> 27) + ((b ~ c) & d ~ b & c) + 0x8F1BBCDC + W[j] + e -- 2^30 * sqrt(5) + e = d + d = c + c = (b<<32 | b & (1<<32)-1) >> 2 + b = a + a = z + end + for j = 61, 80 do + local z = ((a<<32 | a & (1<<32)-1) >> 27) + (b ~ c ~ d) + 0xCA62C1D6 + W[j] + e -- 2^30 * sqrt(10) + e = d + d = c + c = (b<<32 | b & (1<<32)-1) >> 2 + b = a + a = z + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + h5 = e + h5 + end + H[1], H[2], H[3], H[4], H[5] = h1, h2, h3, h4, h5 + end + + local keccak_format_i8 = build_keccak_format("i8") + + local function keccak_feed(lanes, _, str, offs, size, block_size_in_bytes) + -- offs >= 0, size >= 0, size is multiple of block_size_in_bytes, block_size_in_bytes is positive multiple of 8 + local RC = sha3_RC_lo + local qwords_qty = block_size_in_bytes / 8 + local keccak_format = keccak_format_i8[qwords_qty] + for pos = offs + 1, offs + size, block_size_in_bytes do + local qwords_from_message = {string_unpack(keccak_format, str, pos)} + for j = 1, qwords_qty do + lanes[j] = lanes[j] ~ qwords_from_message[j] + end + local L01, L02, L03, L04, L05, L06, L07, L08, L09, L10, L11, L12, L13, L14, L15, L16, L17, L18, L19, L20, L21, L22, L23, L24, L25 = + lanes[1], lanes[2], lanes[3], lanes[4], lanes[5], lanes[6], lanes[7], lanes[8], lanes[9], lanes[10], lanes[11], lanes[12], lanes[13], + lanes[14], lanes[15], lanes[16], lanes[17], lanes[18], lanes[19], lanes[20], lanes[21], lanes[22], lanes[23], lanes[24], lanes[25] + for round_idx = 1, 24 do + local C1 = L01 ~ L06 ~ L11 ~ L16 ~ L21 + local C2 = L02 ~ L07 ~ L12 ~ L17 ~ L22 + local C3 = L03 ~ L08 ~ L13 ~ L18 ~ L23 + local C4 = L04 ~ L09 ~ L14 ~ L19 ~ L24 + local C5 = L05 ~ L10 ~ L15 ~ L20 ~ L25 + local D = C1 ~ C3<<1 ~ C3>>63 + local T0 = D ~ L02 + local T1 = D ~ L07 + local T2 = D ~ L12 + local T3 = D ~ L17 + local T4 = D ~ L22 + L02 = T1<<44 ~ T1>>20 + L07 = T3<<45 ~ T3>>19 + L12 = T0<<1 ~ T0>>63 + L17 = T2<<10 ~ T2>>54 + L22 = T4<<2 ~ T4>>62 + D = C2 ~ C4<<1 ~ C4>>63 + T0 = D ~ L03 + T1 = D ~ L08 + T2 = D ~ L13 + T3 = D ~ L18 + T4 = D ~ L23 + L03 = T2<<43 ~ T2>>21 + L08 = T4<<61 ~ T4>>3 + L13 = T1<<6 ~ T1>>58 + L18 = T3<<15 ~ T3>>49 + L23 = T0<<62 ~ T0>>2 + D = C3 ~ C5<<1 ~ C5>>63 + T0 = D ~ L04 + T1 = D ~ L09 + T2 = D ~ L14 + T3 = D ~ L19 + T4 = D ~ L24 + L04 = T3<<21 ~ T3>>43 + L09 = T0<<28 ~ T0>>36 + L14 = T2<<25 ~ T2>>39 + L19 = T4<<56 ~ T4>>8 + L24 = T1<<55 ~ T1>>9 + D = C4 ~ C1<<1 ~ C1>>63 + T0 = D ~ L05 + T1 = D ~ L10 + T2 = D ~ L15 + T3 = D ~ L20 + T4 = D ~ L25 + L05 = T4<<14 ~ T4>>50 + L10 = T1<<20 ~ T1>>44 + L15 = T3<<8 ~ T3>>56 + L20 = T0<<27 ~ T0>>37 + L25 = T2<<39 ~ T2>>25 + D = C5 ~ C2<<1 ~ C2>>63 + T1 = D ~ L06 + T2 = D ~ L11 + T3 = D ~ L16 + T4 = D ~ L21 + L06 = T2<<3 ~ T2>>61 + L11 = T4<<18 ~ T4>>46 + L16 = T1<<36 ~ T1>>28 + L21 = T3<<41 ~ T3>>23 + L01 = D ~ L01 + L01, L02, L03, L04, L05 = L01 ~ ~L02 & L03, L02 ~ ~L03 & L04, L03 ~ ~L04 & L05, L04 ~ ~L05 & L01, L05 ~ ~L01 & L02 + L06, L07, L08, L09, L10 = L09 ~ ~L10 & L06, L10 ~ ~L06 & L07, L06 ~ ~L07 & L08, L07 ~ ~L08 & L09, L08 ~ ~L09 & L10 + L11, L12, L13, L14, L15 = L12 ~ ~L13 & L14, L13 ~ ~L14 & L15, L14 ~ ~L15 & L11, L15 ~ ~L11 & L12, L11 ~ ~L12 & L13 + L16, L17, L18, L19, L20 = L20 ~ ~L16 & L17, L16 ~ ~L17 & L18, L17 ~ ~L18 & L19, L18 ~ ~L19 & L20, L19 ~ ~L20 & L16 + L21, L22, L23, L24, L25 = L23 ~ ~L24 & L25, L24 ~ ~L25 & L21, L25 ~ ~L21 & L22, L21 ~ ~L22 & L23, L22 ~ ~L23 & L24 + L01 = L01 ~ RC[round_idx] + end + lanes[1] = L01 + lanes[2] = L02 + lanes[3] = L03 + lanes[4] = L04 + lanes[5] = L05 + lanes[6] = L06 + lanes[7] = L07 + lanes[8] = L08 + lanes[9] = L09 + lanes[10] = L10 + lanes[11] = L11 + lanes[12] = L12 + lanes[13] = L13 + lanes[14] = L14 + lanes[15] = L15 + lanes[16] = L16 + lanes[17] = L17 + lanes[18] = L18 + lanes[19] = L19 + lanes[20] = L20 + lanes[21] = L21 + lanes[22] = L22 + lanes[23] = L23 + lanes[24] = L24 + lanes[25] = L25 + end + end + + local function blake2s_feed_64(H, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 64 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> 32 -- t1 = high_4_bytes(bytes_compressed) + if last_block_size then -- flag f0 + vE = ~vE + end + if is_last_node then -- flag f1 + vF = ~vF + end + for j = 1, 10 do + local row = sigma[j] + v0 = v0 + v4 + W[row[1]] + vC = vC ~ v0 + vC = (vC & (1<<32)-1) >> 16 | vC << 16 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = (v4 & (1<<32)-1) >> 12 | v4 << 20 + v0 = v0 + v4 + W[row[2]] + vC = vC ~ v0 + vC = (vC & (1<<32)-1) >> 8 | vC << 24 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = (v4 & (1<<32)-1) >> 7 | v4 << 25 + v1 = v1 + v5 + W[row[3]] + vD = vD ~ v1 + vD = (vD & (1<<32)-1) >> 16 | vD << 16 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = (v5 & (1<<32)-1) >> 12 | v5 << 20 + v1 = v1 + v5 + W[row[4]] + vD = vD ~ v1 + vD = (vD & (1<<32)-1) >> 8 | vD << 24 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = (v5 & (1<<32)-1) >> 7 | v5 << 25 + v2 = v2 + v6 + W[row[5]] + vE = vE ~ v2 + vE = (vE & (1<<32)-1) >> 16 | vE << 16 + vA = vA + vE + v6 = v6 ~ vA + v6 = (v6 & (1<<32)-1) >> 12 | v6 << 20 + v2 = v2 + v6 + W[row[6]] + vE = vE ~ v2 + vE = (vE & (1<<32)-1) >> 8 | vE << 24 + vA = vA + vE + v6 = v6 ~ vA + v6 = (v6 & (1<<32)-1) >> 7 | v6 << 25 + v3 = v3 + v7 + W[row[7]] + vF = vF ~ v3 + vF = (vF & (1<<32)-1) >> 16 | vF << 16 + vB = vB + vF + v7 = v7 ~ vB + v7 = (v7 & (1<<32)-1) >> 12 | v7 << 20 + v3 = v3 + v7 + W[row[8]] + vF = vF ~ v3 + vF = (vF & (1<<32)-1) >> 8 | vF << 24 + vB = vB + vF + v7 = v7 ~ vB + v7 = (v7 & (1<<32)-1) >> 7 | v7 << 25 + v0 = v0 + v5 + W[row[9]] + vF = vF ~ v0 + vF = (vF & (1<<32)-1) >> 16 | vF << 16 + vA = vA + vF + v5 = v5 ~ vA + v5 = (v5 & (1<<32)-1) >> 12 | v5 << 20 + v0 = v0 + v5 + W[row[10]] + vF = vF ~ v0 + vF = (vF & (1<<32)-1) >> 8 | vF << 24 + vA = vA + vF + v5 = v5 ~ vA + v5 = (v5 & (1<<32)-1) >> 7 | v5 << 25 + v1 = v1 + v6 + W[row[11]] + vC = vC ~ v1 + vC = (vC & (1<<32)-1) >> 16 | vC << 16 + vB = vB + vC + v6 = v6 ~ vB + v6 = (v6 & (1<<32)-1) >> 12 | v6 << 20 + v1 = v1 + v6 + W[row[12]] + vC = vC ~ v1 + vC = (vC & (1<<32)-1) >> 8 | vC << 24 + vB = vB + vC + v6 = v6 ~ vB + v6 = (v6 & (1<<32)-1) >> 7 | v6 << 25 + v2 = v2 + v7 + W[row[13]] + vD = vD ~ v2 + vD = (vD & (1<<32)-1) >> 16 | vD << 16 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = (v7 & (1<<32)-1) >> 12 | v7 << 20 + v2 = v2 + v7 + W[row[14]] + vD = vD ~ v2 + vD = (vD & (1<<32)-1) >> 8 | vD << 24 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = (v7 & (1<<32)-1) >> 7 | v7 << 25 + v3 = v3 + v4 + W[row[15]] + vE = vE ~ v3 + vE = (vE & (1<<32)-1) >> 16 | vE << 16 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = (v4 & (1<<32)-1) >> 12 | v4 << 20 + v3 = v3 + v4 + W[row[16]] + vE = vE ~ v3 + vE = (vE & (1<<32)-1) >> 8 | vE << 24 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = (v4 & (1<<32)-1) >> 7 | v4 << 25 + end + h1 = h1 ~ v0 ~ v8 + h2 = h2 ~ v1 ~ v9 + h3 = h3 ~ v2 ~ vA + h4 = h4 ~ v3 ~ vB + h5 = h5 ~ v4 ~ vC + h6 = h6 ~ v5 ~ vD + h7 = h7 ~ v6 ~ vE + h8 = h8 ~ v7 ~ vF + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + local function blake2b_feed_128(H, _, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 128 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> 32 | vC << 32 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 24 | v4 << 40 + v0 = v0 + v4 + W[row[2]] + vC = vC ~ v0 + vC = vC >> 16 | vC << 48 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 63 | v4 << 1 + v1 = v1 + v5 + W[row[3]] + vD = vD ~ v1 + vD = vD >> 32 | vD << 32 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 24 | v5 << 40 + v1 = v1 + v5 + W[row[4]] + vD = vD ~ v1 + vD = vD >> 16 | vD << 48 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 63 | v5 << 1 + v2 = v2 + v6 + W[row[5]] + vE = vE ~ v2 + vE = vE >> 32 | vE << 32 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 24 | v6 << 40 + v2 = v2 + v6 + W[row[6]] + vE = vE ~ v2 + vE = vE >> 16 | vE << 48 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 63 | v6 << 1 + v3 = v3 + v7 + W[row[7]] + vF = vF ~ v3 + vF = vF >> 32 | vF << 32 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 24 | v7 << 40 + v3 = v3 + v7 + W[row[8]] + vF = vF ~ v3 + vF = vF >> 16 | vF << 48 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 63 | v7 << 1 + v0 = v0 + v5 + W[row[9]] + vF = vF ~ v0 + vF = vF >> 32 | vF << 32 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 24 | v5 << 40 + v0 = v0 + v5 + W[row[10]] + vF = vF ~ v0 + vF = vF >> 16 | vF << 48 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 63 | v5 << 1 + v1 = v1 + v6 + W[row[11]] + vC = vC ~ v1 + vC = vC >> 32 | vC << 32 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 24 | v6 << 40 + v1 = v1 + v6 + W[row[12]] + vC = vC ~ v1 + vC = vC >> 16 | vC << 48 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 63 | v6 << 1 + v2 = v2 + v7 + W[row[13]] + vD = vD ~ v2 + vD = vD >> 32 | vD << 32 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 24 | v7 << 40 + v2 = v2 + v7 + W[row[14]] + vD = vD ~ v2 + vD = vD >> 16 | vD << 48 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 63 | v7 << 1 + v3 = v3 + v4 + W[row[15]] + vE = vE ~ v3 + vE = vE >> 32 | vE << 32 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 24 | v4 << 40 + v3 = v3 + v4 + W[row[16]] + vE = vE ~ v3 + vE = vE >> 16 | vE << 48 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 63 | v4 << 1 + end + h1 = h1 ~ v0 ~ v8 + h2 = h2 ~ v1 ~ v9 + h3 = h3 ~ v2 ~ vA + h4 = h4 ~ v3 ~ vB + h5 = h5 ~ v4 ~ vC + h6 = h6 ~ v5 ~ vD + h7 = h7 ~ v6 ~ vE + h8 = h8 ~ v7 ~ vF + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + local function blake3_feed_64(str, offs, size, flags, chunk_index, H_in, H_out, wide_output, block_length) + -- offs >= 0, size >= 0, size is multiple of 64 + block_length = block_length or 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H_in[1], H_in[2], H_in[3], H_in[4], H_in[5], H_in[6], H_in[7], H_in[8] + H_out = H_out or H_in + for pos = offs + 1, offs + size, 64 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> 16 | vC << 16 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = (v4 & (1<<32)-1) >> 12 | v4 << 20 + v0 = v0 + v4 + W[perm_blake3[j + 14]] + vC = vC ~ v0 + vC = (vC & (1<<32)-1) >> 8 | vC << 24 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = (v4 & (1<<32)-1) >> 7 | v4 << 25 + v1 = v1 + v5 + W[perm_blake3[j + 1]] + vD = vD ~ v1 + vD = (vD & (1<<32)-1) >> 16 | vD << 16 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = (v5 & (1<<32)-1) >> 12 | v5 << 20 + v1 = v1 + v5 + W[perm_blake3[j + 2]] + vD = vD ~ v1 + vD = (vD & (1<<32)-1) >> 8 | vD << 24 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = (v5 & (1<<32)-1) >> 7 | v5 << 25 + v2 = v2 + v6 + W[perm_blake3[j + 16]] + vE = vE ~ v2 + vE = (vE & (1<<32)-1) >> 16 | vE << 16 + vA = vA + vE + v6 = v6 ~ vA + v6 = (v6 & (1<<32)-1) >> 12 | v6 << 20 + v2 = v2 + v6 + W[perm_blake3[j + 7]] + vE = vE ~ v2 + vE = (vE & (1<<32)-1) >> 8 | vE << 24 + vA = vA + vE + v6 = v6 ~ vA + v6 = (v6 & (1<<32)-1) >> 7 | v6 << 25 + v3 = v3 + v7 + W[perm_blake3[j + 15]] + vF = vF ~ v3 + vF = (vF & (1<<32)-1) >> 16 | vF << 16 + vB = vB + vF + v7 = v7 ~ vB + v7 = (v7 & (1<<32)-1) >> 12 | v7 << 20 + v3 = v3 + v7 + W[perm_blake3[j + 17]] + vF = vF ~ v3 + vF = (vF & (1<<32)-1) >> 8 | vF << 24 + vB = vB + vF + v7 = v7 ~ vB + v7 = (v7 & (1<<32)-1) >> 7 | v7 << 25 + v0 = v0 + v5 + W[perm_blake3[j + 21]] + vF = vF ~ v0 + vF = (vF & (1<<32)-1) >> 16 | vF << 16 + vA = vA + vF + v5 = v5 ~ vA + v5 = (v5 & (1<<32)-1) >> 12 | v5 << 20 + v0 = v0 + v5 + W[perm_blake3[j + 5]] + vF = vF ~ v0 + vF = (vF & (1<<32)-1) >> 8 | vF << 24 + vA = vA + vF + v5 = v5 ~ vA + v5 = (v5 & (1<<32)-1) >> 7 | v5 << 25 + v1 = v1 + v6 + W[perm_blake3[j + 3]] + vC = vC ~ v1 + vC = (vC & (1<<32)-1) >> 16 | vC << 16 + vB = vB + vC + v6 = v6 ~ vB + v6 = (v6 & (1<<32)-1) >> 12 | v6 << 20 + v1 = v1 + v6 + W[perm_blake3[j + 6]] + vC = vC ~ v1 + vC = (vC & (1<<32)-1) >> 8 | vC << 24 + vB = vB + vC + v6 = v6 ~ vB + v6 = (v6 & (1<<32)-1) >> 7 | v6 << 25 + v2 = v2 + v7 + W[perm_blake3[j + 4]] + vD = vD ~ v2 + vD = (vD & (1<<32)-1) >> 16 | vD << 16 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = (v7 & (1<<32)-1) >> 12 | v7 << 20 + v2 = v2 + v7 + W[perm_blake3[j + 18]] + vD = vD ~ v2 + vD = (vD & (1<<32)-1) >> 8 | vD << 24 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = (v7 & (1<<32)-1) >> 7 | v7 << 25 + v3 = v3 + v4 + W[perm_blake3[j + 19]] + vE = vE ~ v3 + vE = (vE & (1<<32)-1) >> 16 | vE << 16 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = (v4 & (1<<32)-1) >> 12 | v4 << 20 + v3 = v3 + v4 + W[perm_blake3[j + 20]] + vE = vE ~ v3 + vE = (vE & (1<<32)-1) >> 8 | vE << 24 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = (v4 & (1<<32)-1) >> 7 | v4 << 25 + end + if wide_output then + H_out[ 9] = h1 ~ v8 + H_out[10] = h2 ~ v9 + H_out[11] = h3 ~ vA + H_out[12] = h4 ~ vB + H_out[13] = h5 ~ vC + H_out[14] = h6 ~ vD + H_out[15] = h7 ~ vE + H_out[16] = h8 ~ vF + end + h1 = v0 ~ v8 + h2 = v1 ~ v9 + h3 = v2 ~ vA + h4 = v3 ~ vB + h5 = v4 ~ vC + h6 = v5 ~ vD + h7 = v6 ~ vE + h8 = v7 ~ vF + end + H_out[1], H_out[2], H_out[3], H_out[4], H_out[5], H_out[6], H_out[7], H_out[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + return HEX64, XORA5, XOR_BYTE, sha256_feed_64, sha512_feed_128, md5_feed_64, sha1_feed_64, keccak_feed, blake2s_feed_64, blake2b_feed_128, blake3_feed_64 + ]=](md5_next_shift, md5_K, sha2_K_lo, sha2_K_hi, build_keccak_format, sha3_RC_lo, sigma, common_W, sha2_H_lo, sha2_H_hi, perm_blake3) + +end + + +if branch == "INT32" then + + + -- implementation for Lua 5.3/5.4 having non-standard numbers config "int32"+"double" (built with LUA_INT_TYPE=LUA_INT_INT) + + K_lo_modulo = 2^32 + + function HEX(x) -- returns string of 8 lowercase hexadecimal digits + return string_format("%08x", x) + end + + XORA5, XOR_BYTE, sha256_feed_64, sha512_feed_128, md5_feed_64, sha1_feed_64, keccak_feed, blake2s_feed_64, blake2b_feed_128, blake3_feed_64 = load[=[-- branch "INT32" + local md5_next_shift, md5_K, sha2_K_lo, sha2_K_hi, build_keccak_format, sha3_RC_lo, sha3_RC_hi, sigma, common_W, sha2_H_lo, sha2_H_hi, perm_blake3 = ... + local string_unpack, floor = string.unpack, math.floor + + local function XORA5(x, y) + return x ~ (y and (y + 2^31) % 2^32 - 2^31 or 0xA5A5A5A5) + end + + local function XOR_BYTE(x, y) + return x ~ y + end + + local function sha256_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W, sha2_K_hi + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4", str, pos) + for j = 17, 64 do + local a, b = W[j-15], W[j-2] + W[j] = (a>>7 ~ a<<25 ~ a<<14 ~ a>>18 ~ a>>3) + (b<<15 ~ b>>17 ~ b<<13 ~ b>>19 ~ b>>10) + W[j-7] + W[j-16] + end + local a, b, c, d, e, f, g, h = h1, h2, h3, h4, h5, h6, h7, h8 + for j = 1, 64 do + local z = (e>>6 ~ e<<26 ~ e>>11 ~ e<<21 ~ e>>25 ~ e<<7) + (g ~ e & (f ~ g)) + h + K[j] + W[j] + h = g + g = f + f = e + e = z + d + d = c + c = b + b = a + a = z + ((a ~ c) & d ~ a & c) + (a>>2 ~ a<<30 ~ a>>13 ~ a<<19 ~ a<<10 ~ a>>22) + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + h5 = e + h5 + h6 = f + h6 + h7 = g + h7 + h8 = h + h8 + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + local function sha512_feed_128(H_lo, H_hi, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + -- W1_hi, W1_lo, W2_hi, W2_lo, ... Wk_hi = W[2*k-1], Wk_lo = W[2*k] + local floor, W, K_lo, K_hi = floor, common_W, sha2_K_lo, sha2_K_hi + local h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for pos = offs + 1, offs + size, 128 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16], + W[17], W[18], W[19], W[20], W[21], W[22], W[23], W[24], W[25], W[26], W[27], W[28], W[29], W[30], W[31], W[32] = + string_unpack(">i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4", str, pos) + for jj = 17*2, 80*2, 2 do + local a_lo, a_hi, b_lo, b_hi = W[jj-30], W[jj-31], W[jj-4], W[jj-5] + local tmp = + (a_lo>>1 ~ a_hi<<31 ~ a_lo>>8 ~ a_hi<<24 ~ a_lo>>7 ~ a_hi<<25) % 2^32 + + (b_lo>>19 ~ b_hi<<13 ~ b_lo<<3 ~ b_hi>>29 ~ b_lo>>6 ~ b_hi<<26) % 2^32 + + W[jj-14] % 2^32 + W[jj-32] % 2^32 + W[jj-1] = + (a_hi>>1 ~ a_lo<<31 ~ a_hi>>8 ~ a_lo<<24 ~ a_hi>>7) + + (b_hi>>19 ~ b_lo<<13 ~ b_hi<<3 ~ b_lo>>29 ~ b_hi>>6) + + W[jj-15] + W[jj-33] + floor(tmp / 2^32) + W[jj] = 0|((tmp + 2^31) % 2^32 - 2^31) + end + local a_lo, b_lo, c_lo, d_lo, e_lo, f_lo, g_lo, h_lo = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + local a_hi, b_hi, c_hi, d_hi, e_hi, f_hi, g_hi, h_hi = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + for j = 1, 80 do + local jj = 2*j + local z_lo = (e_lo>>14 ~ e_hi<<18 ~ e_lo>>18 ~ e_hi<<14 ~ e_lo<<23 ~ e_hi>>9) % 2^32 + (g_lo ~ e_lo & (f_lo ~ g_lo)) % 2^32 + h_lo % 2^32 + K_lo[j] + W[jj] % 2^32 + local z_hi = (e_hi>>14 ~ e_lo<<18 ~ e_hi>>18 ~ e_lo<<14 ~ e_hi<<23 ~ e_lo>>9) + (g_hi ~ e_hi & (f_hi ~ g_hi)) + h_hi + K_hi[j] + W[jj-1] + floor(z_lo / 2^32) + z_lo = z_lo % 2^32 + h_lo = g_lo; h_hi = g_hi + g_lo = f_lo; g_hi = f_hi + f_lo = e_lo; f_hi = e_hi + e_lo = z_lo + d_lo % 2^32 + e_hi = z_hi + d_hi + floor(e_lo / 2^32) + e_lo = 0|((e_lo + 2^31) % 2^32 - 2^31) + d_lo = c_lo; d_hi = c_hi + c_lo = b_lo; c_hi = b_hi + b_lo = a_lo; b_hi = a_hi + z_lo = z_lo + (d_lo & c_lo ~ b_lo & (d_lo ~ c_lo)) % 2^32 + (b_lo>>28 ~ b_hi<<4 ~ b_lo<<30 ~ b_hi>>2 ~ b_lo<<25 ~ b_hi>>7) % 2^32 + a_hi = z_hi + (d_hi & c_hi ~ b_hi & (d_hi ~ c_hi)) + (b_hi>>28 ~ b_lo<<4 ~ b_hi<<30 ~ b_lo>>2 ~ b_hi<<25 ~ b_lo>>7) + floor(z_lo / 2^32) + a_lo = 0|((z_lo + 2^31) % 2^32 - 2^31) + end + a_lo = h1_lo % 2^32 + a_lo % 2^32 + h1_hi = h1_hi + a_hi + floor(a_lo / 2^32) + h1_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h2_lo % 2^32 + b_lo % 2^32 + h2_hi = h2_hi + b_hi + floor(a_lo / 2^32) + h2_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h3_lo % 2^32 + c_lo % 2^32 + h3_hi = h3_hi + c_hi + floor(a_lo / 2^32) + h3_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h4_lo % 2^32 + d_lo % 2^32 + h4_hi = h4_hi + d_hi + floor(a_lo / 2^32) + h4_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h5_lo % 2^32 + e_lo % 2^32 + h5_hi = h5_hi + e_hi + floor(a_lo / 2^32) + h5_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h6_lo % 2^32 + f_lo % 2^32 + h6_hi = h6_hi + f_hi + floor(a_lo / 2^32) + h6_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h7_lo % 2^32 + g_lo % 2^32 + h7_hi = h7_hi + g_hi + floor(a_lo / 2^32) + h7_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + a_lo = h8_lo % 2^32 + h_lo % 2^32 + h8_hi = h8_hi + h_hi + floor(a_lo / 2^32) + h8_lo = 0|((a_lo + 2^31) % 2^32 - 2^31) + end + H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + end + + local function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K, md5_next_shift = common_W, md5_K, md5_next_shift + local h1, h2, h3, h4 = H[1], H[2], H[3], H[4] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">s) + b + s = md5_next_shift[s] + end + s = 32-5 + for j = 17, 32 do + local F = (c ~ d & (b ~ c)) + a + K[j] + W[(5*j-4 & 15) + 1] + a = d + d = c + c = b + b = (F << 32-s | F>>s) + b + s = md5_next_shift[s] + end + s = 32-4 + for j = 33, 48 do + local F = (b ~ c ~ d) + a + K[j] + W[(3*j+2 & 15) + 1] + a = d + d = c + c = b + b = (F << 32-s | F>>s) + b + s = md5_next_shift[s] + end + s = 32-6 + for j = 49, 64 do + local F = (c ~ (b | ~d)) + a + K[j] + W[(j*7-7 & 15) + 1] + a = d + d = c + c = b + b = (F << 32-s | F>>s) + b + s = md5_next_shift[s] + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + end + H[1], H[2], H[3], H[4] = h1, h2, h3, h4 + end + + local function sha1_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5 = H[1], H[2], H[3], H[4], H[5] + for pos = offs + 1, offs + size, 64 do + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack(">i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4i4", str, pos) + for j = 17, 80 do + local a = W[j-3] ~ W[j-8] ~ W[j-14] ~ W[j-16] + W[j] = a << 1 ~ a >> 31 + end + local a, b, c, d, e = h1, h2, h3, h4, h5 + for j = 1, 20 do + local z = (a << 5 ~ a >> 27) + (d ~ b & (c ~ d)) + 0x5A827999 + W[j] + e -- constant = floor(2^30 * sqrt(2)) + e = d + d = c + c = b << 30 ~ b >> 2 + b = a + a = z + end + for j = 21, 40 do + local z = (a << 5 ~ a >> 27) + (b ~ c ~ d) + 0x6ED9EBA1 + W[j] + e -- 2^30 * sqrt(3) + e = d + d = c + c = b << 30 ~ b >> 2 + b = a + a = z + end + for j = 41, 60 do + local z = (a << 5 ~ a >> 27) + ((b ~ c) & d ~ b & c) + 0x8F1BBCDC + W[j] + e -- 2^30 * sqrt(5) + e = d + d = c + c = b << 30 ~ b >> 2 + b = a + a = z + end + for j = 61, 80 do + local z = (a << 5 ~ a >> 27) + (b ~ c ~ d) + 0xCA62C1D6 + W[j] + e -- 2^30 * sqrt(10) + e = d + d = c + c = b << 30 ~ b >> 2 + b = a + a = z + end + h1 = a + h1 + h2 = b + h2 + h3 = c + h3 + h4 = d + h4 + h5 = e + h5 + end + H[1], H[2], H[3], H[4], H[5] = h1, h2, h3, h4, h5 + end + + local keccak_format_i4i4 = build_keccak_format("i4i4") + + local function keccak_feed(lanes_lo, lanes_hi, str, offs, size, block_size_in_bytes) + -- offs >= 0, size >= 0, size is multiple of block_size_in_bytes, block_size_in_bytes is positive multiple of 8 + local RC_lo, RC_hi = sha3_RC_lo, sha3_RC_hi + local qwords_qty = block_size_in_bytes / 8 + local keccak_format = keccak_format_i4i4[qwords_qty] + for pos = offs + 1, offs + size, block_size_in_bytes do + local dwords_from_message = {string_unpack(keccak_format, str, pos)} + for j = 1, qwords_qty do + lanes_lo[j] = lanes_lo[j] ~ dwords_from_message[2*j-1] + lanes_hi[j] = lanes_hi[j] ~ dwords_from_message[2*j] + end + local L01_lo, L01_hi, L02_lo, L02_hi, L03_lo, L03_hi, L04_lo, L04_hi, L05_lo, L05_hi, L06_lo, L06_hi, L07_lo, L07_hi, L08_lo, L08_hi, + L09_lo, L09_hi, L10_lo, L10_hi, L11_lo, L11_hi, L12_lo, L12_hi, L13_lo, L13_hi, L14_lo, L14_hi, L15_lo, L15_hi, L16_lo, L16_hi, + L17_lo, L17_hi, L18_lo, L18_hi, L19_lo, L19_hi, L20_lo, L20_hi, L21_lo, L21_hi, L22_lo, L22_hi, L23_lo, L23_hi, L24_lo, L24_hi, L25_lo, L25_hi = + lanes_lo[1], lanes_hi[1], lanes_lo[2], lanes_hi[2], lanes_lo[3], lanes_hi[3], lanes_lo[4], lanes_hi[4], lanes_lo[5], lanes_hi[5], + lanes_lo[6], lanes_hi[6], lanes_lo[7], lanes_hi[7], lanes_lo[8], lanes_hi[8], lanes_lo[9], lanes_hi[9], lanes_lo[10], lanes_hi[10], + lanes_lo[11], lanes_hi[11], lanes_lo[12], lanes_hi[12], lanes_lo[13], lanes_hi[13], lanes_lo[14], lanes_hi[14], lanes_lo[15], lanes_hi[15], + lanes_lo[16], lanes_hi[16], lanes_lo[17], lanes_hi[17], lanes_lo[18], lanes_hi[18], lanes_lo[19], lanes_hi[19], lanes_lo[20], lanes_hi[20], + lanes_lo[21], lanes_hi[21], lanes_lo[22], lanes_hi[22], lanes_lo[23], lanes_hi[23], lanes_lo[24], lanes_hi[24], lanes_lo[25], lanes_hi[25] + for round_idx = 1, 24 do + local C1_lo = L01_lo ~ L06_lo ~ L11_lo ~ L16_lo ~ L21_lo + local C1_hi = L01_hi ~ L06_hi ~ L11_hi ~ L16_hi ~ L21_hi + local C2_lo = L02_lo ~ L07_lo ~ L12_lo ~ L17_lo ~ L22_lo + local C2_hi = L02_hi ~ L07_hi ~ L12_hi ~ L17_hi ~ L22_hi + local C3_lo = L03_lo ~ L08_lo ~ L13_lo ~ L18_lo ~ L23_lo + local C3_hi = L03_hi ~ L08_hi ~ L13_hi ~ L18_hi ~ L23_hi + local C4_lo = L04_lo ~ L09_lo ~ L14_lo ~ L19_lo ~ L24_lo + local C4_hi = L04_hi ~ L09_hi ~ L14_hi ~ L19_hi ~ L24_hi + local C5_lo = L05_lo ~ L10_lo ~ L15_lo ~ L20_lo ~ L25_lo + local C5_hi = L05_hi ~ L10_hi ~ L15_hi ~ L20_hi ~ L25_hi + local D_lo = C1_lo ~ C3_lo<<1 ~ C3_hi>>31 + local D_hi = C1_hi ~ C3_hi<<1 ~ C3_lo>>31 + local T0_lo = D_lo ~ L02_lo + local T0_hi = D_hi ~ L02_hi + local T1_lo = D_lo ~ L07_lo + local T1_hi = D_hi ~ L07_hi + local T2_lo = D_lo ~ L12_lo + local T2_hi = D_hi ~ L12_hi + local T3_lo = D_lo ~ L17_lo + local T3_hi = D_hi ~ L17_hi + local T4_lo = D_lo ~ L22_lo + local T4_hi = D_hi ~ L22_hi + L02_lo = T1_lo>>20 ~ T1_hi<<12 + L02_hi = T1_hi>>20 ~ T1_lo<<12 + L07_lo = T3_lo>>19 ~ T3_hi<<13 + L07_hi = T3_hi>>19 ~ T3_lo<<13 + L12_lo = T0_lo<<1 ~ T0_hi>>31 + L12_hi = T0_hi<<1 ~ T0_lo>>31 + L17_lo = T2_lo<<10 ~ T2_hi>>22 + L17_hi = T2_hi<<10 ~ T2_lo>>22 + L22_lo = T4_lo<<2 ~ T4_hi>>30 + L22_hi = T4_hi<<2 ~ T4_lo>>30 + D_lo = C2_lo ~ C4_lo<<1 ~ C4_hi>>31 + D_hi = C2_hi ~ C4_hi<<1 ~ C4_lo>>31 + T0_lo = D_lo ~ L03_lo + T0_hi = D_hi ~ L03_hi + T1_lo = D_lo ~ L08_lo + T1_hi = D_hi ~ L08_hi + T2_lo = D_lo ~ L13_lo + T2_hi = D_hi ~ L13_hi + T3_lo = D_lo ~ L18_lo + T3_hi = D_hi ~ L18_hi + T4_lo = D_lo ~ L23_lo + T4_hi = D_hi ~ L23_hi + L03_lo = T2_lo>>21 ~ T2_hi<<11 + L03_hi = T2_hi>>21 ~ T2_lo<<11 + L08_lo = T4_lo>>3 ~ T4_hi<<29 + L08_hi = T4_hi>>3 ~ T4_lo<<29 + L13_lo = T1_lo<<6 ~ T1_hi>>26 + L13_hi = T1_hi<<6 ~ T1_lo>>26 + L18_lo = T3_lo<<15 ~ T3_hi>>17 + L18_hi = T3_hi<<15 ~ T3_lo>>17 + L23_lo = T0_lo>>2 ~ T0_hi<<30 + L23_hi = T0_hi>>2 ~ T0_lo<<30 + D_lo = C3_lo ~ C5_lo<<1 ~ C5_hi>>31 + D_hi = C3_hi ~ C5_hi<<1 ~ C5_lo>>31 + T0_lo = D_lo ~ L04_lo + T0_hi = D_hi ~ L04_hi + T1_lo = D_lo ~ L09_lo + T1_hi = D_hi ~ L09_hi + T2_lo = D_lo ~ L14_lo + T2_hi = D_hi ~ L14_hi + T3_lo = D_lo ~ L19_lo + T3_hi = D_hi ~ L19_hi + T4_lo = D_lo ~ L24_lo + T4_hi = D_hi ~ L24_hi + L04_lo = T3_lo<<21 ~ T3_hi>>11 + L04_hi = T3_hi<<21 ~ T3_lo>>11 + L09_lo = T0_lo<<28 ~ T0_hi>>4 + L09_hi = T0_hi<<28 ~ T0_lo>>4 + L14_lo = T2_lo<<25 ~ T2_hi>>7 + L14_hi = T2_hi<<25 ~ T2_lo>>7 + L19_lo = T4_lo>>8 ~ T4_hi<<24 + L19_hi = T4_hi>>8 ~ T4_lo<<24 + L24_lo = T1_lo>>9 ~ T1_hi<<23 + L24_hi = T1_hi>>9 ~ T1_lo<<23 + D_lo = C4_lo ~ C1_lo<<1 ~ C1_hi>>31 + D_hi = C4_hi ~ C1_hi<<1 ~ C1_lo>>31 + T0_lo = D_lo ~ L05_lo + T0_hi = D_hi ~ L05_hi + T1_lo = D_lo ~ L10_lo + T1_hi = D_hi ~ L10_hi + T2_lo = D_lo ~ L15_lo + T2_hi = D_hi ~ L15_hi + T3_lo = D_lo ~ L20_lo + T3_hi = D_hi ~ L20_hi + T4_lo = D_lo ~ L25_lo + T4_hi = D_hi ~ L25_hi + L05_lo = T4_lo<<14 ~ T4_hi>>18 + L05_hi = T4_hi<<14 ~ T4_lo>>18 + L10_lo = T1_lo<<20 ~ T1_hi>>12 + L10_hi = T1_hi<<20 ~ T1_lo>>12 + L15_lo = T3_lo<<8 ~ T3_hi>>24 + L15_hi = T3_hi<<8 ~ T3_lo>>24 + L20_lo = T0_lo<<27 ~ T0_hi>>5 + L20_hi = T0_hi<<27 ~ T0_lo>>5 + L25_lo = T2_lo>>25 ~ T2_hi<<7 + L25_hi = T2_hi>>25 ~ T2_lo<<7 + D_lo = C5_lo ~ C2_lo<<1 ~ C2_hi>>31 + D_hi = C5_hi ~ C2_hi<<1 ~ C2_lo>>31 + T1_lo = D_lo ~ L06_lo + T1_hi = D_hi ~ L06_hi + T2_lo = D_lo ~ L11_lo + T2_hi = D_hi ~ L11_hi + T3_lo = D_lo ~ L16_lo + T3_hi = D_hi ~ L16_hi + T4_lo = D_lo ~ L21_lo + T4_hi = D_hi ~ L21_hi + L06_lo = T2_lo<<3 ~ T2_hi>>29 + L06_hi = T2_hi<<3 ~ T2_lo>>29 + L11_lo = T4_lo<<18 ~ T4_hi>>14 + L11_hi = T4_hi<<18 ~ T4_lo>>14 + L16_lo = T1_lo>>28 ~ T1_hi<<4 + L16_hi = T1_hi>>28 ~ T1_lo<<4 + L21_lo = T3_lo>>23 ~ T3_hi<<9 + L21_hi = T3_hi>>23 ~ T3_lo<<9 + L01_lo = D_lo ~ L01_lo + L01_hi = D_hi ~ L01_hi + L01_lo, L02_lo, L03_lo, L04_lo, L05_lo = L01_lo ~ ~L02_lo & L03_lo, L02_lo ~ ~L03_lo & L04_lo, L03_lo ~ ~L04_lo & L05_lo, L04_lo ~ ~L05_lo & L01_lo, L05_lo ~ ~L01_lo & L02_lo + L01_hi, L02_hi, L03_hi, L04_hi, L05_hi = L01_hi ~ ~L02_hi & L03_hi, L02_hi ~ ~L03_hi & L04_hi, L03_hi ~ ~L04_hi & L05_hi, L04_hi ~ ~L05_hi & L01_hi, L05_hi ~ ~L01_hi & L02_hi + L06_lo, L07_lo, L08_lo, L09_lo, L10_lo = L09_lo ~ ~L10_lo & L06_lo, L10_lo ~ ~L06_lo & L07_lo, L06_lo ~ ~L07_lo & L08_lo, L07_lo ~ ~L08_lo & L09_lo, L08_lo ~ ~L09_lo & L10_lo + L06_hi, L07_hi, L08_hi, L09_hi, L10_hi = L09_hi ~ ~L10_hi & L06_hi, L10_hi ~ ~L06_hi & L07_hi, L06_hi ~ ~L07_hi & L08_hi, L07_hi ~ ~L08_hi & L09_hi, L08_hi ~ ~L09_hi & L10_hi + L11_lo, L12_lo, L13_lo, L14_lo, L15_lo = L12_lo ~ ~L13_lo & L14_lo, L13_lo ~ ~L14_lo & L15_lo, L14_lo ~ ~L15_lo & L11_lo, L15_lo ~ ~L11_lo & L12_lo, L11_lo ~ ~L12_lo & L13_lo + L11_hi, L12_hi, L13_hi, L14_hi, L15_hi = L12_hi ~ ~L13_hi & L14_hi, L13_hi ~ ~L14_hi & L15_hi, L14_hi ~ ~L15_hi & L11_hi, L15_hi ~ ~L11_hi & L12_hi, L11_hi ~ ~L12_hi & L13_hi + L16_lo, L17_lo, L18_lo, L19_lo, L20_lo = L20_lo ~ ~L16_lo & L17_lo, L16_lo ~ ~L17_lo & L18_lo, L17_lo ~ ~L18_lo & L19_lo, L18_lo ~ ~L19_lo & L20_lo, L19_lo ~ ~L20_lo & L16_lo + L16_hi, L17_hi, L18_hi, L19_hi, L20_hi = L20_hi ~ ~L16_hi & L17_hi, L16_hi ~ ~L17_hi & L18_hi, L17_hi ~ ~L18_hi & L19_hi, L18_hi ~ ~L19_hi & L20_hi, L19_hi ~ ~L20_hi & L16_hi + L21_lo, L22_lo, L23_lo, L24_lo, L25_lo = L23_lo ~ ~L24_lo & L25_lo, L24_lo ~ ~L25_lo & L21_lo, L25_lo ~ ~L21_lo & L22_lo, L21_lo ~ ~L22_lo & L23_lo, L22_lo ~ ~L23_lo & L24_lo + L21_hi, L22_hi, L23_hi, L24_hi, L25_hi = L23_hi ~ ~L24_hi & L25_hi, L24_hi ~ ~L25_hi & L21_hi, L25_hi ~ ~L21_hi & L22_hi, L21_hi ~ ~L22_hi & L23_hi, L22_hi ~ ~L23_hi & L24_hi + L01_lo = L01_lo ~ RC_lo[round_idx] + L01_hi = L01_hi ~ RC_hi[round_idx] + end + lanes_lo[1] = L01_lo; lanes_hi[1] = L01_hi + lanes_lo[2] = L02_lo; lanes_hi[2] = L02_hi + lanes_lo[3] = L03_lo; lanes_hi[3] = L03_hi + lanes_lo[4] = L04_lo; lanes_hi[4] = L04_hi + lanes_lo[5] = L05_lo; lanes_hi[5] = L05_hi + lanes_lo[6] = L06_lo; lanes_hi[6] = L06_hi + lanes_lo[7] = L07_lo; lanes_hi[7] = L07_hi + lanes_lo[8] = L08_lo; lanes_hi[8] = L08_hi + lanes_lo[9] = L09_lo; lanes_hi[9] = L09_hi + lanes_lo[10] = L10_lo; lanes_hi[10] = L10_hi + lanes_lo[11] = L11_lo; lanes_hi[11] = L11_hi + lanes_lo[12] = L12_lo; lanes_hi[12] = L12_hi + lanes_lo[13] = L13_lo; lanes_hi[13] = L13_hi + lanes_lo[14] = L14_lo; lanes_hi[14] = L14_hi + lanes_lo[15] = L15_lo; lanes_hi[15] = L15_hi + lanes_lo[16] = L16_lo; lanes_hi[16] = L16_hi + lanes_lo[17] = L17_lo; lanes_hi[17] = L17_hi + lanes_lo[18] = L18_lo; lanes_hi[18] = L18_hi + lanes_lo[19] = L19_lo; lanes_hi[19] = L19_hi + lanes_lo[20] = L20_lo; lanes_hi[20] = L20_hi + lanes_lo[21] = L21_lo; lanes_hi[21] = L21_hi + lanes_lo[22] = L22_lo; lanes_hi[22] = L22_hi + lanes_lo[23] = L23_lo; lanes_hi[23] = L23_hi + lanes_lo[24] = L24_lo; lanes_hi[24] = L24_hi + lanes_lo[25] = L25_lo; lanes_hi[25] = L25_hi + end + end + + local function blake2s_feed_64(H, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs + 1, offs + size, 64 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> 16 | vC << 16 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 12 | v4 << 20 + v0 = v0 + v4 + W[row[2]] + vC = vC ~ v0 + vC = vC >> 8 | vC << 24 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 7 | v4 << 25 + v1 = v1 + v5 + W[row[3]] + vD = vD ~ v1 + vD = vD >> 16 | vD << 16 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 12 | v5 << 20 + v1 = v1 + v5 + W[row[4]] + vD = vD ~ v1 + vD = vD >> 8 | vD << 24 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 7 | v5 << 25 + v2 = v2 + v6 + W[row[5]] + vE = vE ~ v2 + vE = vE >> 16 | vE << 16 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 12 | v6 << 20 + v2 = v2 + v6 + W[row[6]] + vE = vE ~ v2 + vE = vE >> 8 | vE << 24 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 7 | v6 << 25 + v3 = v3 + v7 + W[row[7]] + vF = vF ~ v3 + vF = vF >> 16 | vF << 16 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 12 | v7 << 20 + v3 = v3 + v7 + W[row[8]] + vF = vF ~ v3 + vF = vF >> 8 | vF << 24 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 7 | v7 << 25 + v0 = v0 + v5 + W[row[9]] + vF = vF ~ v0 + vF = vF >> 16 | vF << 16 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 12 | v5 << 20 + v0 = v0 + v5 + W[row[10]] + vF = vF ~ v0 + vF = vF >> 8 | vF << 24 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 7 | v5 << 25 + v1 = v1 + v6 + W[row[11]] + vC = vC ~ v1 + vC = vC >> 16 | vC << 16 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 12 | v6 << 20 + v1 = v1 + v6 + W[row[12]] + vC = vC ~ v1 + vC = vC >> 8 | vC << 24 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 7 | v6 << 25 + v2 = v2 + v7 + W[row[13]] + vD = vD ~ v2 + vD = vD >> 16 | vD << 16 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 12 | v7 << 20 + v2 = v2 + v7 + W[row[14]] + vD = vD ~ v2 + vD = vD >> 8 | vD << 24 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 7 | v7 << 25 + v3 = v3 + v4 + W[row[15]] + vE = vE ~ v3 + vE = vE >> 16 | vE << 16 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 12 | v4 << 20 + v3 = v3 + v4 + W[row[16]] + vE = vE ~ v3 + vE = vE >> 8 | vE << 24 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 7 | v4 << 25 + end + h1 = h1 ~ v0 ~ v8 + h2 = h2 ~ v1 ~ v9 + h3 = h3 ~ v2 ~ vA + h4 = h4 ~ v3 ~ vB + h5 = h5 ~ v4 ~ vC + h6 = h6 ~ v5 ~ vD + h7 = h7 ~ v6 ~ vE + h8 = h8 ~ v7 ~ vF + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + local function blake2b_feed_128(H_lo, H_hi, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local W = common_W + local h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for pos = offs + 1, offs + size, 128 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16], + W[17], W[18], W[19], W[20], W[21], W[22], W[23], W[24], W[25], W[26], W[27], W[28], W[29], W[30], W[31], W[32] = + string_unpack("> 24 | v4_hi << 8, v4_hi >> 24 | v4_lo << 8 + k = row[2] * 2 + v0_lo = v0_lo % 2^32 + v4_lo % 2^32 + W[k-1] % 2^32 + v0_hi = v0_hi + v4_hi + floor(v0_lo / 2^32) + W[k] + v0_lo = 0|((v0_lo + 2^31) % 2^32 - 2^31) + vC_lo, vC_hi = vC_lo ~ v0_lo, vC_hi ~ v0_hi + vC_lo, vC_hi = vC_lo >> 16 | vC_hi << 16, vC_hi >> 16 | vC_lo << 16 + v8_lo = v8_lo % 2^32 + vC_lo % 2^32 + v8_hi = v8_hi + vC_hi + floor(v8_lo / 2^32) + v8_lo = 0|((v8_lo + 2^31) % 2^32 - 2^31) + v4_lo, v4_hi = v4_lo ~ v8_lo, v4_hi ~ v8_hi + v4_lo, v4_hi = v4_lo << 1 | v4_hi >> 31, v4_hi << 1 | v4_lo >> 31 + k = row[3] * 2 + v1_lo = v1_lo % 2^32 + v5_lo % 2^32 + W[k-1] % 2^32 + v1_hi = v1_hi + v5_hi + floor(v1_lo / 2^32) + W[k] + v1_lo = 0|((v1_lo + 2^31) % 2^32 - 2^31) + vD_lo, vD_hi = vD_hi ~ v1_hi, vD_lo ~ v1_lo + v9_lo = v9_lo % 2^32 + vD_lo % 2^32 + v9_hi = v9_hi + vD_hi + floor(v9_lo / 2^32) + v9_lo = 0|((v9_lo + 2^31) % 2^32 - 2^31) + v5_lo, v5_hi = v5_lo ~ v9_lo, v5_hi ~ v9_hi + v5_lo, v5_hi = v5_lo >> 24 | v5_hi << 8, v5_hi >> 24 | v5_lo << 8 + k = row[4] * 2 + v1_lo = v1_lo % 2^32 + v5_lo % 2^32 + W[k-1] % 2^32 + v1_hi = v1_hi + v5_hi + floor(v1_lo / 2^32) + W[k] + v1_lo = 0|((v1_lo + 2^31) % 2^32 - 2^31) + vD_lo, vD_hi = vD_lo ~ v1_lo, vD_hi ~ v1_hi + vD_lo, vD_hi = vD_lo >> 16 | vD_hi << 16, vD_hi >> 16 | vD_lo << 16 + v9_lo = v9_lo % 2^32 + vD_lo % 2^32 + v9_hi = v9_hi + vD_hi + floor(v9_lo / 2^32) + v9_lo = 0|((v9_lo + 2^31) % 2^32 - 2^31) + v5_lo, v5_hi = v5_lo ~ v9_lo, v5_hi ~ v9_hi + v5_lo, v5_hi = v5_lo << 1 | v5_hi >> 31, v5_hi << 1 | v5_lo >> 31 + k = row[5] * 2 + v2_lo = v2_lo % 2^32 + v6_lo % 2^32 + W[k-1] % 2^32 + v2_hi = v2_hi + v6_hi + floor(v2_lo / 2^32) + W[k] + v2_lo = 0|((v2_lo + 2^31) % 2^32 - 2^31) + vE_lo, vE_hi = vE_hi ~ v2_hi, vE_lo ~ v2_lo + vA_lo = vA_lo % 2^32 + vE_lo % 2^32 + vA_hi = vA_hi + vE_hi + floor(vA_lo / 2^32) + vA_lo = 0|((vA_lo + 2^31) % 2^32 - 2^31) + v6_lo, v6_hi = v6_lo ~ vA_lo, v6_hi ~ vA_hi + v6_lo, v6_hi = v6_lo >> 24 | v6_hi << 8, v6_hi >> 24 | v6_lo << 8 + k = row[6] * 2 + v2_lo = v2_lo % 2^32 + v6_lo % 2^32 + W[k-1] % 2^32 + v2_hi = v2_hi + v6_hi + floor(v2_lo / 2^32) + W[k] + v2_lo = 0|((v2_lo + 2^31) % 2^32 - 2^31) + vE_lo, vE_hi = vE_lo ~ v2_lo, vE_hi ~ v2_hi + vE_lo, vE_hi = vE_lo >> 16 | vE_hi << 16, vE_hi >> 16 | vE_lo << 16 + vA_lo = vA_lo % 2^32 + vE_lo % 2^32 + vA_hi = vA_hi + vE_hi + floor(vA_lo / 2^32) + vA_lo = 0|((vA_lo + 2^31) % 2^32 - 2^31) + v6_lo, v6_hi = v6_lo ~ vA_lo, v6_hi ~ vA_hi + v6_lo, v6_hi = v6_lo << 1 | v6_hi >> 31, v6_hi << 1 | v6_lo >> 31 + k = row[7] * 2 + v3_lo = v3_lo % 2^32 + v7_lo % 2^32 + W[k-1] % 2^32 + v3_hi = v3_hi + v7_hi + floor(v3_lo / 2^32) + W[k] + v3_lo = 0|((v3_lo + 2^31) % 2^32 - 2^31) + vF_lo, vF_hi = vF_hi ~ v3_hi, vF_lo ~ v3_lo + vB_lo = vB_lo % 2^32 + vF_lo % 2^32 + vB_hi = vB_hi + vF_hi + floor(vB_lo / 2^32) + vB_lo = 0|((vB_lo + 2^31) % 2^32 - 2^31) + v7_lo, v7_hi = v7_lo ~ vB_lo, v7_hi ~ vB_hi + v7_lo, v7_hi = v7_lo >> 24 | v7_hi << 8, v7_hi >> 24 | v7_lo << 8 + k = row[8] * 2 + v3_lo = v3_lo % 2^32 + v7_lo % 2^32 + W[k-1] % 2^32 + v3_hi = v3_hi + v7_hi + floor(v3_lo / 2^32) + W[k] + v3_lo = 0|((v3_lo + 2^31) % 2^32 - 2^31) + vF_lo, vF_hi = vF_lo ~ v3_lo, vF_hi ~ v3_hi + vF_lo, vF_hi = vF_lo >> 16 | vF_hi << 16, vF_hi >> 16 | vF_lo << 16 + vB_lo = vB_lo % 2^32 + vF_lo % 2^32 + vB_hi = vB_hi + vF_hi + floor(vB_lo / 2^32) + vB_lo = 0|((vB_lo + 2^31) % 2^32 - 2^31) + v7_lo, v7_hi = v7_lo ~ vB_lo, v7_hi ~ vB_hi + v7_lo, v7_hi = v7_lo << 1 | v7_hi >> 31, v7_hi << 1 | v7_lo >> 31 + k = row[9] * 2 + v0_lo = v0_lo % 2^32 + v5_lo % 2^32 + W[k-1] % 2^32 + v0_hi = v0_hi + v5_hi + floor(v0_lo / 2^32) + W[k] + v0_lo = 0|((v0_lo + 2^31) % 2^32 - 2^31) + vF_lo, vF_hi = vF_hi ~ v0_hi, vF_lo ~ v0_lo + vA_lo = vA_lo % 2^32 + vF_lo % 2^32 + vA_hi = vA_hi + vF_hi + floor(vA_lo / 2^32) + vA_lo = 0|((vA_lo + 2^31) % 2^32 - 2^31) + v5_lo, v5_hi = v5_lo ~ vA_lo, v5_hi ~ vA_hi + v5_lo, v5_hi = v5_lo >> 24 | v5_hi << 8, v5_hi >> 24 | v5_lo << 8 + k = row[10] * 2 + v0_lo = v0_lo % 2^32 + v5_lo % 2^32 + W[k-1] % 2^32 + v0_hi = v0_hi + v5_hi + floor(v0_lo / 2^32) + W[k] + v0_lo = 0|((v0_lo + 2^31) % 2^32 - 2^31) + vF_lo, vF_hi = vF_lo ~ v0_lo, vF_hi ~ v0_hi + vF_lo, vF_hi = vF_lo >> 16 | vF_hi << 16, vF_hi >> 16 | vF_lo << 16 + vA_lo = vA_lo % 2^32 + vF_lo % 2^32 + vA_hi = vA_hi + vF_hi + floor(vA_lo / 2^32) + vA_lo = 0|((vA_lo + 2^31) % 2^32 - 2^31) + v5_lo, v5_hi = v5_lo ~ vA_lo, v5_hi ~ vA_hi + v5_lo, v5_hi = v5_lo << 1 | v5_hi >> 31, v5_hi << 1 | v5_lo >> 31 + k = row[11] * 2 + v1_lo = v1_lo % 2^32 + v6_lo % 2^32 + W[k-1] % 2^32 + v1_hi = v1_hi + v6_hi + floor(v1_lo / 2^32) + W[k] + v1_lo = 0|((v1_lo + 2^31) % 2^32 - 2^31) + vC_lo, vC_hi = vC_hi ~ v1_hi, vC_lo ~ v1_lo + vB_lo = vB_lo % 2^32 + vC_lo % 2^32 + vB_hi = vB_hi + vC_hi + floor(vB_lo / 2^32) + vB_lo = 0|((vB_lo + 2^31) % 2^32 - 2^31) + v6_lo, v6_hi = v6_lo ~ vB_lo, v6_hi ~ vB_hi + v6_lo, v6_hi = v6_lo >> 24 | v6_hi << 8, v6_hi >> 24 | v6_lo << 8 + k = row[12] * 2 + v1_lo = v1_lo % 2^32 + v6_lo % 2^32 + W[k-1] % 2^32 + v1_hi = v1_hi + v6_hi + floor(v1_lo / 2^32) + W[k] + v1_lo = 0|((v1_lo + 2^31) % 2^32 - 2^31) + vC_lo, vC_hi = vC_lo ~ v1_lo, vC_hi ~ v1_hi + vC_lo, vC_hi = vC_lo >> 16 | vC_hi << 16, vC_hi >> 16 | vC_lo << 16 + vB_lo = vB_lo % 2^32 + vC_lo % 2^32 + vB_hi = vB_hi + vC_hi + floor(vB_lo / 2^32) + vB_lo = 0|((vB_lo + 2^31) % 2^32 - 2^31) + v6_lo, v6_hi = v6_lo ~ vB_lo, v6_hi ~ vB_hi + v6_lo, v6_hi = v6_lo << 1 | v6_hi >> 31, v6_hi << 1 | v6_lo >> 31 + k = row[13] * 2 + v2_lo = v2_lo % 2^32 + v7_lo % 2^32 + W[k-1] % 2^32 + v2_hi = v2_hi + v7_hi + floor(v2_lo / 2^32) + W[k] + v2_lo = 0|((v2_lo + 2^31) % 2^32 - 2^31) + vD_lo, vD_hi = vD_hi ~ v2_hi, vD_lo ~ v2_lo + v8_lo = v8_lo % 2^32 + vD_lo % 2^32 + v8_hi = v8_hi + vD_hi + floor(v8_lo / 2^32) + v8_lo = 0|((v8_lo + 2^31) % 2^32 - 2^31) + v7_lo, v7_hi = v7_lo ~ v8_lo, v7_hi ~ v8_hi + v7_lo, v7_hi = v7_lo >> 24 | v7_hi << 8, v7_hi >> 24 | v7_lo << 8 + k = row[14] * 2 + v2_lo = v2_lo % 2^32 + v7_lo % 2^32 + W[k-1] % 2^32 + v2_hi = v2_hi + v7_hi + floor(v2_lo / 2^32) + W[k] + v2_lo = 0|((v2_lo + 2^31) % 2^32 - 2^31) + vD_lo, vD_hi = vD_lo ~ v2_lo, vD_hi ~ v2_hi + vD_lo, vD_hi = vD_lo >> 16 | vD_hi << 16, vD_hi >> 16 | vD_lo << 16 + v8_lo = v8_lo % 2^32 + vD_lo % 2^32 + v8_hi = v8_hi + vD_hi + floor(v8_lo / 2^32) + v8_lo = 0|((v8_lo + 2^31) % 2^32 - 2^31) + v7_lo, v7_hi = v7_lo ~ v8_lo, v7_hi ~ v8_hi + v7_lo, v7_hi = v7_lo << 1 | v7_hi >> 31, v7_hi << 1 | v7_lo >> 31 + k = row[15] * 2 + v3_lo = v3_lo % 2^32 + v4_lo % 2^32 + W[k-1] % 2^32 + v3_hi = v3_hi + v4_hi + floor(v3_lo / 2^32) + W[k] + v3_lo = 0|((v3_lo + 2^31) % 2^32 - 2^31) + vE_lo, vE_hi = vE_hi ~ v3_hi, vE_lo ~ v3_lo + v9_lo = v9_lo % 2^32 + vE_lo % 2^32 + v9_hi = v9_hi + vE_hi + floor(v9_lo / 2^32) + v9_lo = 0|((v9_lo + 2^31) % 2^32 - 2^31) + v4_lo, v4_hi = v4_lo ~ v9_lo, v4_hi ~ v9_hi + v4_lo, v4_hi = v4_lo >> 24 | v4_hi << 8, v4_hi >> 24 | v4_lo << 8 + k = row[16] * 2 + v3_lo = v3_lo % 2^32 + v4_lo % 2^32 + W[k-1] % 2^32 + v3_hi = v3_hi + v4_hi + floor(v3_lo / 2^32) + W[k] + v3_lo = 0|((v3_lo + 2^31) % 2^32 - 2^31) + vE_lo, vE_hi = vE_lo ~ v3_lo, vE_hi ~ v3_hi + vE_lo, vE_hi = vE_lo >> 16 | vE_hi << 16, vE_hi >> 16 | vE_lo << 16 + v9_lo = v9_lo % 2^32 + vE_lo % 2^32 + v9_hi = v9_hi + vE_hi + floor(v9_lo / 2^32) + v9_lo = 0|((v9_lo + 2^31) % 2^32 - 2^31) + v4_lo, v4_hi = v4_lo ~ v9_lo, v4_hi ~ v9_hi + v4_lo, v4_hi = v4_lo << 1 | v4_hi >> 31, v4_hi << 1 | v4_lo >> 31 + end + h1_lo = h1_lo ~ v0_lo ~ v8_lo + h2_lo = h2_lo ~ v1_lo ~ v9_lo + h3_lo = h3_lo ~ v2_lo ~ vA_lo + h4_lo = h4_lo ~ v3_lo ~ vB_lo + h5_lo = h5_lo ~ v4_lo ~ vC_lo + h6_lo = h6_lo ~ v5_lo ~ vD_lo + h7_lo = h7_lo ~ v6_lo ~ vE_lo + h8_lo = h8_lo ~ v7_lo ~ vF_lo + h1_hi = h1_hi ~ v0_hi ~ v8_hi + h2_hi = h2_hi ~ v1_hi ~ v9_hi + h3_hi = h3_hi ~ v2_hi ~ vA_hi + h4_hi = h4_hi ~ v3_hi ~ vB_hi + h5_hi = h5_hi ~ v4_hi ~ vC_hi + h6_hi = h6_hi ~ v5_hi ~ vD_hi + h7_hi = h7_hi ~ v6_hi ~ vE_hi + h8_hi = h8_hi ~ v7_hi ~ vF_hi + end + H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + return bytes_compressed + end + + local function blake3_feed_64(str, offs, size, flags, chunk_index, H_in, H_out, wide_output, block_length) + -- offs >= 0, size >= 0, size is multiple of 64 + block_length = block_length or 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H_in[1], H_in[2], H_in[3], H_in[4], H_in[5], H_in[6], H_in[7], H_in[8] + H_out = H_out or H_in + for pos = offs + 1, offs + size, 64 do + if str then + W[1], W[2], W[3], W[4], W[5], W[6], W[7], W[8], W[9], W[10], W[11], W[12], W[13], W[14], W[15], W[16] = + string_unpack("> 16 | vC << 16 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 12 | v4 << 20 + v0 = v0 + v4 + W[perm_blake3[j + 14]] + vC = vC ~ v0 + vC = vC >> 8 | vC << 24 + v8 = v8 + vC + v4 = v4 ~ v8 + v4 = v4 >> 7 | v4 << 25 + v1 = v1 + v5 + W[perm_blake3[j + 1]] + vD = vD ~ v1 + vD = vD >> 16 | vD << 16 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 12 | v5 << 20 + v1 = v1 + v5 + W[perm_blake3[j + 2]] + vD = vD ~ v1 + vD = vD >> 8 | vD << 24 + v9 = v9 + vD + v5 = v5 ~ v9 + v5 = v5 >> 7 | v5 << 25 + v2 = v2 + v6 + W[perm_blake3[j + 16]] + vE = vE ~ v2 + vE = vE >> 16 | vE << 16 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 12 | v6 << 20 + v2 = v2 + v6 + W[perm_blake3[j + 7]] + vE = vE ~ v2 + vE = vE >> 8 | vE << 24 + vA = vA + vE + v6 = v6 ~ vA + v6 = v6 >> 7 | v6 << 25 + v3 = v3 + v7 + W[perm_blake3[j + 15]] + vF = vF ~ v3 + vF = vF >> 16 | vF << 16 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 12 | v7 << 20 + v3 = v3 + v7 + W[perm_blake3[j + 17]] + vF = vF ~ v3 + vF = vF >> 8 | vF << 24 + vB = vB + vF + v7 = v7 ~ vB + v7 = v7 >> 7 | v7 << 25 + v0 = v0 + v5 + W[perm_blake3[j + 21]] + vF = vF ~ v0 + vF = vF >> 16 | vF << 16 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 12 | v5 << 20 + v0 = v0 + v5 + W[perm_blake3[j + 5]] + vF = vF ~ v0 + vF = vF >> 8 | vF << 24 + vA = vA + vF + v5 = v5 ~ vA + v5 = v5 >> 7 | v5 << 25 + v1 = v1 + v6 + W[perm_blake3[j + 3]] + vC = vC ~ v1 + vC = vC >> 16 | vC << 16 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 12 | v6 << 20 + v1 = v1 + v6 + W[perm_blake3[j + 6]] + vC = vC ~ v1 + vC = vC >> 8 | vC << 24 + vB = vB + vC + v6 = v6 ~ vB + v6 = v6 >> 7 | v6 << 25 + v2 = v2 + v7 + W[perm_blake3[j + 4]] + vD = vD ~ v2 + vD = vD >> 16 | vD << 16 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 12 | v7 << 20 + v2 = v2 + v7 + W[perm_blake3[j + 18]] + vD = vD ~ v2 + vD = vD >> 8 | vD << 24 + v8 = v8 + vD + v7 = v7 ~ v8 + v7 = v7 >> 7 | v7 << 25 + v3 = v3 + v4 + W[perm_blake3[j + 19]] + vE = vE ~ v3 + vE = vE >> 16 | vE << 16 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 12 | v4 << 20 + v3 = v3 + v4 + W[perm_blake3[j + 20]] + vE = vE ~ v3 + vE = vE >> 8 | vE << 24 + v9 = v9 + vE + v4 = v4 ~ v9 + v4 = v4 >> 7 | v4 << 25 + end + if wide_output then + H_out[ 9] = h1 ~ v8 + H_out[10] = h2 ~ v9 + H_out[11] = h3 ~ vA + H_out[12] = h4 ~ vB + H_out[13] = h5 ~ vC + H_out[14] = h6 ~ vD + H_out[15] = h7 ~ vE + H_out[16] = h8 ~ vF + end + h1 = v0 ~ v8 + h2 = v1 ~ v9 + h3 = v2 ~ vA + h4 = v3 ~ vB + h5 = v4 ~ vC + h6 = v5 ~ vD + h7 = v6 ~ vE + h8 = v7 ~ vF + end + H_out[1], H_out[2], H_out[3], H_out[4], H_out[5], H_out[6], H_out[7], H_out[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + return XORA5, XOR_BYTE, sha256_feed_64, sha512_feed_128, md5_feed_64, sha1_feed_64, keccak_feed, blake2s_feed_64, blake2b_feed_128, blake3_feed_64 + ]=](md5_next_shift, md5_K, sha2_K_lo, sha2_K_hi, build_keccak_format, sha3_RC_lo, sha3_RC_hi, sigma, common_W, sha2_H_lo, sha2_H_hi, perm_blake3) + +end + +XOR = XOR or XORA5 + +if branch == "LIB32" or branch == "EMUL" then + + + -- implementation for Lua 5.1/5.2 (with or without bitwise library available) + + function sha256_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K = common_W, sha2_K_hi + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((a * 256 + b) * 256 + c) * 256 + d + end + for j = 17, 64 do + local a, b = W[j-15], W[j-2] + local a7, a18, b17, b19 = a / 2^7, a / 2^18, b / 2^17, b / 2^19 + W[j] = (XOR(a7 % 1 * (2^32 - 1) + a7, a18 % 1 * (2^32 - 1) + a18, (a - a % 2^3) / 2^3) + W[j-16] + W[j-7] + + XOR(b17 % 1 * (2^32 - 1) + b17, b19 % 1 * (2^32 - 1) + b19, (b - b % 2^10) / 2^10)) % 2^32 + end + local a, b, c, d, e, f, g, h = h1, h2, h3, h4, h5, h6, h7, h8 + for j = 1, 64 do + e = e % 2^32 + local e6, e11, e7 = e / 2^6, e / 2^11, e * 2^7 + local e7_lo = e7 % 2^32 + local z = AND(e, f) + AND(-1-e, g) + h + K[j] + W[j] + + XOR(e6 % 1 * (2^32 - 1) + e6, e11 % 1 * (2^32 - 1) + e11, e7_lo + (e7 - e7_lo) / 2^32) + h = g + g = f + f = e + e = z + d + d = c + c = b + b = a % 2^32 + local b2, b13, b10 = b / 2^2, b / 2^13, b * 2^10 + local b10_lo = b10 % 2^32 + a = z + AND(d, c) + AND(b, XOR(d, c)) + + XOR(b2 % 1 * (2^32 - 1) + b2, b13 % 1 * (2^32 - 1) + b13, b10_lo + (b10 - b10_lo) / 2^32) + end + h1, h2, h3, h4 = (a + h1) % 2^32, (b + h2) % 2^32, (c + h3) % 2^32, (d + h4) % 2^32 + h5, h6, h7, h8 = (e + h5) % 2^32, (f + h6) % 2^32, (g + h7) % 2^32, (h + h8) % 2^32 + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + + + function sha512_feed_128(H_lo, H_hi, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 128 + -- W1_hi, W1_lo, W2_hi, W2_lo, ... Wk_hi = W[2*k-1], Wk_lo = W[2*k] + local W, K_lo, K_hi = common_W, sha2_K_lo, sha2_K_hi + local h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for pos = offs, offs + size - 1, 128 do + for j = 1, 16*2 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((a * 256 + b) * 256 + c) * 256 + d + end + for jj = 17*2, 80*2, 2 do + local a_hi, a_lo, b_hi, b_lo = W[jj-31], W[jj-30], W[jj-5], W[jj-4] + local b_hi_6, b_hi_19, b_hi_29, b_lo_19, b_lo_29, a_hi_1, a_hi_7, a_hi_8, a_lo_1, a_lo_8 = + b_hi % 2^6, b_hi % 2^19, b_hi % 2^29, b_lo % 2^19, b_lo % 2^29, a_hi % 2^1, a_hi % 2^7, a_hi % 2^8, a_lo % 2^1, a_lo % 2^8 + local tmp1 = XOR((a_lo - a_lo_1) / 2^1 + a_hi_1 * 2^31, (a_lo - a_lo_8) / 2^8 + a_hi_8 * 2^24, (a_lo - a_lo % 2^7) / 2^7 + a_hi_7 * 2^25) % 2^32 + + XOR((b_lo - b_lo_19) / 2^19 + b_hi_19 * 2^13, b_lo_29 * 2^3 + (b_hi - b_hi_29) / 2^29, (b_lo - b_lo % 2^6) / 2^6 + b_hi_6 * 2^26) % 2^32 + + W[jj-14] + W[jj-32] + local tmp2 = tmp1 % 2^32 + W[jj-1] = (XOR((a_hi - a_hi_1) / 2^1 + a_lo_1 * 2^31, (a_hi - a_hi_8) / 2^8 + a_lo_8 * 2^24, (a_hi - a_hi_7) / 2^7) + + XOR((b_hi - b_hi_19) / 2^19 + b_lo_19 * 2^13, b_hi_29 * 2^3 + (b_lo - b_lo_29) / 2^29, (b_hi - b_hi_6) / 2^6) + + W[jj-15] + W[jj-33] + (tmp1 - tmp2) / 2^32) % 2^32 + W[jj] = tmp2 + end + local a_lo, b_lo, c_lo, d_lo, e_lo, f_lo, g_lo, h_lo = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + local a_hi, b_hi, c_hi, d_hi, e_hi, f_hi, g_hi, h_hi = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + for j = 1, 80 do + local jj = 2*j + local e_lo_9, e_lo_14, e_lo_18, e_hi_9, e_hi_14, e_hi_18 = e_lo % 2^9, e_lo % 2^14, e_lo % 2^18, e_hi % 2^9, e_hi % 2^14, e_hi % 2^18 + local tmp1 = (AND(e_lo, f_lo) + AND(-1-e_lo, g_lo)) % 2^32 + h_lo + K_lo[j] + W[jj] + + XOR((e_lo - e_lo_14) / 2^14 + e_hi_14 * 2^18, (e_lo - e_lo_18) / 2^18 + e_hi_18 * 2^14, e_lo_9 * 2^23 + (e_hi - e_hi_9) / 2^9) % 2^32 + local z_lo = tmp1 % 2^32 + local z_hi = AND(e_hi, f_hi) + AND(-1-e_hi, g_hi) + h_hi + K_hi[j] + W[jj-1] + (tmp1 - z_lo) / 2^32 + + XOR((e_hi - e_hi_14) / 2^14 + e_lo_14 * 2^18, (e_hi - e_hi_18) / 2^18 + e_lo_18 * 2^14, e_hi_9 * 2^23 + (e_lo - e_lo_9) / 2^9) + h_lo = g_lo; h_hi = g_hi + g_lo = f_lo; g_hi = f_hi + f_lo = e_lo; f_hi = e_hi + tmp1 = z_lo + d_lo + e_lo = tmp1 % 2^32 + e_hi = (z_hi + d_hi + (tmp1 - e_lo) / 2^32) % 2^32 + d_lo = c_lo; d_hi = c_hi + c_lo = b_lo; c_hi = b_hi + b_lo = a_lo; b_hi = a_hi + local b_lo_2, b_lo_7, b_lo_28, b_hi_2, b_hi_7, b_hi_28 = b_lo % 2^2, b_lo % 2^7, b_lo % 2^28, b_hi % 2^2, b_hi % 2^7, b_hi % 2^28 + tmp1 = z_lo + (AND(d_lo, c_lo) + AND(b_lo, XOR(d_lo, c_lo))) % 2^32 + + XOR((b_lo - b_lo_28) / 2^28 + b_hi_28 * 2^4, b_lo_2 * 2^30 + (b_hi - b_hi_2) / 2^2, b_lo_7 * 2^25 + (b_hi - b_hi_7) / 2^7) % 2^32 + a_lo = tmp1 % 2^32 + a_hi = (z_hi + AND(d_hi, c_hi) + AND(b_hi, XOR(d_hi, c_hi)) + (tmp1 - a_lo) / 2^32 + + XOR((b_hi - b_hi_28) / 2^28 + b_lo_28 * 2^4, b_hi_2 * 2^30 + (b_lo - b_lo_2) / 2^2, b_hi_7 * 2^25 + (b_lo - b_lo_7) / 2^7)) % 2^32 + end + a_lo = h1_lo + a_lo + h1_lo = a_lo % 2^32 + h1_hi = (h1_hi + a_hi + (a_lo - h1_lo) / 2^32) % 2^32 + a_lo = h2_lo + b_lo + h2_lo = a_lo % 2^32 + h2_hi = (h2_hi + b_hi + (a_lo - h2_lo) / 2^32) % 2^32 + a_lo = h3_lo + c_lo + h3_lo = a_lo % 2^32 + h3_hi = (h3_hi + c_hi + (a_lo - h3_lo) / 2^32) % 2^32 + a_lo = h4_lo + d_lo + h4_lo = a_lo % 2^32 + h4_hi = (h4_hi + d_hi + (a_lo - h4_lo) / 2^32) % 2^32 + a_lo = h5_lo + e_lo + h5_lo = a_lo % 2^32 + h5_hi = (h5_hi + e_hi + (a_lo - h5_lo) / 2^32) % 2^32 + a_lo = h6_lo + f_lo + h6_lo = a_lo % 2^32 + h6_hi = (h6_hi + f_hi + (a_lo - h6_lo) / 2^32) % 2^32 + a_lo = h7_lo + g_lo + h7_lo = a_lo % 2^32 + h7_hi = (h7_hi + g_hi + (a_lo - h7_lo) / 2^32) % 2^32 + a_lo = h8_lo + h_lo + h8_lo = a_lo % 2^32 + h8_hi = (h8_hi + h_hi + (a_lo - h8_lo) / 2^32) % 2^32 + end + H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + end + + + if branch == "LIB32" then + + function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K, md5_next_shift = common_W, md5_K, md5_next_shift + local h1, h2, h3, h4 = H[1], H[2], H[3], H[4] + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + local a, b, c, d = h1, h2, h3, h4 + local s = 25 + for j = 1, 16 do + local F = ROR(AND(b, c) + AND(-1-b, d) + a + K[j] + W[j], s) + b + s = md5_next_shift[s] + a = d + d = c + c = b + b = F + end + s = 27 + for j = 17, 32 do + local F = ROR(AND(d, b) + AND(-1-d, c) + a + K[j] + W[(5*j-4) % 16 + 1], s) + b + s = md5_next_shift[s] + a = d + d = c + c = b + b = F + end + s = 28 + for j = 33, 48 do + local F = ROR(XOR(XOR(b, c), d) + a + K[j] + W[(3*j+2) % 16 + 1], s) + b + s = md5_next_shift[s] + a = d + d = c + c = b + b = F + end + s = 26 + for j = 49, 64 do + local F = ROR(XOR(c, OR(b, -1-d)) + a + K[j] + W[(j*7-7) % 16 + 1], s) + b + s = md5_next_shift[s] + a = d + d = c + c = b + b = F + end + h1 = (a + h1) % 2^32 + h2 = (b + h2) % 2^32 + h3 = (c + h3) % 2^32 + h4 = (d + h4) % 2^32 + end + H[1], H[2], H[3], H[4] = h1, h2, h3, h4 + end + + elseif branch == "EMUL" then + + function md5_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W, K, md5_next_shift = common_W, md5_K, md5_next_shift + local h1, h2, h3, h4 = H[1], H[2], H[3], H[4] + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + local a, b, c, d = h1, h2, h3, h4 + local s = 25 + for j = 1, 16 do + local z = (AND(b, c) + AND(-1-b, d) + a + K[j] + W[j]) % 2^32 / 2^s + local y = z % 1 + s = md5_next_shift[s] + a = d + d = c + c = b + b = y * 2^32 + (z - y) + b + end + s = 27 + for j = 17, 32 do + local z = (AND(d, b) + AND(-1-d, c) + a + K[j] + W[(5*j-4) % 16 + 1]) % 2^32 / 2^s + local y = z % 1 + s = md5_next_shift[s] + a = d + d = c + c = b + b = y * 2^32 + (z - y) + b + end + s = 28 + for j = 33, 48 do + local z = (XOR(XOR(b, c), d) + a + K[j] + W[(3*j+2) % 16 + 1]) % 2^32 / 2^s + local y = z % 1 + s = md5_next_shift[s] + a = d + d = c + c = b + b = y * 2^32 + (z - y) + b + end + s = 26 + for j = 49, 64 do + local z = (XOR(c, OR(b, -1-d)) + a + K[j] + W[(j*7-7) % 16 + 1]) % 2^32 / 2^s + local y = z % 1 + s = md5_next_shift[s] + a = d + d = c + c = b + b = y * 2^32 + (z - y) + b + end + h1 = (a + h1) % 2^32 + h2 = (b + h2) % 2^32 + h3 = (c + h3) % 2^32 + h4 = (d + h4) % 2^32 + end + H[1], H[2], H[3], H[4] = h1, h2, h3, h4 + end + + end + + + function sha1_feed_64(H, str, offs, size) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5 = H[1], H[2], H[3], H[4], H[5] + for pos = offs, offs + size - 1, 64 do + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((a * 256 + b) * 256 + c) * 256 + d + end + for j = 17, 80 do + local a = XOR(W[j-3], W[j-8], W[j-14], W[j-16]) % 2^32 * 2 + local b = a % 2^32 + W[j] = b + (a - b) / 2^32 + end + local a, b, c, d, e = h1, h2, h3, h4, h5 + for j = 1, 20 do + local a5 = a * 2^5 + local z = a5 % 2^32 + z = z + (a5 - z) / 2^32 + AND(b, c) + AND(-1-b, d) + 0x5A827999 + W[j] + e -- constant = floor(2^30 * sqrt(2)) + e = d + d = c + c = b / 2^2 + c = c % 1 * (2^32 - 1) + c + b = a + a = z % 2^32 + end + for j = 21, 40 do + local a5 = a * 2^5 + local z = a5 % 2^32 + z = z + (a5 - z) / 2^32 + XOR(b, c, d) + 0x6ED9EBA1 + W[j] + e -- 2^30 * sqrt(3) + e = d + d = c + c = b / 2^2 + c = c % 1 * (2^32 - 1) + c + b = a + a = z % 2^32 + end + for j = 41, 60 do + local a5 = a * 2^5 + local z = a5 % 2^32 + z = z + (a5 - z) / 2^32 + AND(d, c) + AND(b, XOR(d, c)) + 0x8F1BBCDC + W[j] + e -- 2^30 * sqrt(5) + e = d + d = c + c = b / 2^2 + c = c % 1 * (2^32 - 1) + c + b = a + a = z % 2^32 + end + for j = 61, 80 do + local a5 = a * 2^5 + local z = a5 % 2^32 + z = z + (a5 - z) / 2^32 + XOR(b, c, d) + 0xCA62C1D6 + W[j] + e -- 2^30 * sqrt(10) + e = d + d = c + c = b / 2^2 + c = c % 1 * (2^32 - 1) + c + b = a + a = z % 2^32 + end + h1 = (a + h1) % 2^32 + h2 = (b + h2) % 2^32 + h3 = (c + h3) % 2^32 + h4 = (d + h4) % 2^32 + h5 = (e + h5) % 2^32 + end + H[1], H[2], H[3], H[4], H[5] = h1, h2, h3, h4, h5 + end + + + function keccak_feed(lanes_lo, lanes_hi, str, offs, size, block_size_in_bytes) + -- This is an example of a Lua function having 79 local variables :-) + -- offs >= 0, size >= 0, size is multiple of block_size_in_bytes, block_size_in_bytes is positive multiple of 8 + local RC_lo, RC_hi = sha3_RC_lo, sha3_RC_hi + local qwords_qty = block_size_in_bytes / 8 + for pos = offs, offs + size - 1, block_size_in_bytes do + for j = 1, qwords_qty do + local a, b, c, d = byte(str, pos + 1, pos + 4) + lanes_lo[j] = XOR(lanes_lo[j], ((d * 256 + c) * 256 + b) * 256 + a) + pos = pos + 8 + a, b, c, d = byte(str, pos - 3, pos) + lanes_hi[j] = XOR(lanes_hi[j], ((d * 256 + c) * 256 + b) * 256 + a) + end + local L01_lo, L01_hi, L02_lo, L02_hi, L03_lo, L03_hi, L04_lo, L04_hi, L05_lo, L05_hi, L06_lo, L06_hi, L07_lo, L07_hi, L08_lo, L08_hi, + L09_lo, L09_hi, L10_lo, L10_hi, L11_lo, L11_hi, L12_lo, L12_hi, L13_lo, L13_hi, L14_lo, L14_hi, L15_lo, L15_hi, L16_lo, L16_hi, + L17_lo, L17_hi, L18_lo, L18_hi, L19_lo, L19_hi, L20_lo, L20_hi, L21_lo, L21_hi, L22_lo, L22_hi, L23_lo, L23_hi, L24_lo, L24_hi, L25_lo, L25_hi = + lanes_lo[1], lanes_hi[1], lanes_lo[2], lanes_hi[2], lanes_lo[3], lanes_hi[3], lanes_lo[4], lanes_hi[4], lanes_lo[5], lanes_hi[5], + lanes_lo[6], lanes_hi[6], lanes_lo[7], lanes_hi[7], lanes_lo[8], lanes_hi[8], lanes_lo[9], lanes_hi[9], lanes_lo[10], lanes_hi[10], + lanes_lo[11], lanes_hi[11], lanes_lo[12], lanes_hi[12], lanes_lo[13], lanes_hi[13], lanes_lo[14], lanes_hi[14], lanes_lo[15], lanes_hi[15], + lanes_lo[16], lanes_hi[16], lanes_lo[17], lanes_hi[17], lanes_lo[18], lanes_hi[18], lanes_lo[19], lanes_hi[19], lanes_lo[20], lanes_hi[20], + lanes_lo[21], lanes_hi[21], lanes_lo[22], lanes_hi[22], lanes_lo[23], lanes_hi[23], lanes_lo[24], lanes_hi[24], lanes_lo[25], lanes_hi[25] + for round_idx = 1, 24 do + local C1_lo = XOR(L01_lo, L06_lo, L11_lo, L16_lo, L21_lo) + local C1_hi = XOR(L01_hi, L06_hi, L11_hi, L16_hi, L21_hi) + local C2_lo = XOR(L02_lo, L07_lo, L12_lo, L17_lo, L22_lo) + local C2_hi = XOR(L02_hi, L07_hi, L12_hi, L17_hi, L22_hi) + local C3_lo = XOR(L03_lo, L08_lo, L13_lo, L18_lo, L23_lo) + local C3_hi = XOR(L03_hi, L08_hi, L13_hi, L18_hi, L23_hi) + local C4_lo = XOR(L04_lo, L09_lo, L14_lo, L19_lo, L24_lo) + local C4_hi = XOR(L04_hi, L09_hi, L14_hi, L19_hi, L24_hi) + local C5_lo = XOR(L05_lo, L10_lo, L15_lo, L20_lo, L25_lo) + local C5_hi = XOR(L05_hi, L10_hi, L15_hi, L20_hi, L25_hi) + local D_lo = XOR(C1_lo, C3_lo * 2 + (C3_hi % 2^32 - C3_hi % 2^31) / 2^31) + local D_hi = XOR(C1_hi, C3_hi * 2 + (C3_lo % 2^32 - C3_lo % 2^31) / 2^31) + local T0_lo = XOR(D_lo, L02_lo) + local T0_hi = XOR(D_hi, L02_hi) + local T1_lo = XOR(D_lo, L07_lo) + local T1_hi = XOR(D_hi, L07_hi) + local T2_lo = XOR(D_lo, L12_lo) + local T2_hi = XOR(D_hi, L12_hi) + local T3_lo = XOR(D_lo, L17_lo) + local T3_hi = XOR(D_hi, L17_hi) + local T4_lo = XOR(D_lo, L22_lo) + local T4_hi = XOR(D_hi, L22_hi) + L02_lo = (T1_lo % 2^32 - T1_lo % 2^20) / 2^20 + T1_hi * 2^12 + L02_hi = (T1_hi % 2^32 - T1_hi % 2^20) / 2^20 + T1_lo * 2^12 + L07_lo = (T3_lo % 2^32 - T3_lo % 2^19) / 2^19 + T3_hi * 2^13 + L07_hi = (T3_hi % 2^32 - T3_hi % 2^19) / 2^19 + T3_lo * 2^13 + L12_lo = T0_lo * 2 + (T0_hi % 2^32 - T0_hi % 2^31) / 2^31 + L12_hi = T0_hi * 2 + (T0_lo % 2^32 - T0_lo % 2^31) / 2^31 + L17_lo = T2_lo * 2^10 + (T2_hi % 2^32 - T2_hi % 2^22) / 2^22 + L17_hi = T2_hi * 2^10 + (T2_lo % 2^32 - T2_lo % 2^22) / 2^22 + L22_lo = T4_lo * 2^2 + (T4_hi % 2^32 - T4_hi % 2^30) / 2^30 + L22_hi = T4_hi * 2^2 + (T4_lo % 2^32 - T4_lo % 2^30) / 2^30 + D_lo = XOR(C2_lo, C4_lo * 2 + (C4_hi % 2^32 - C4_hi % 2^31) / 2^31) + D_hi = XOR(C2_hi, C4_hi * 2 + (C4_lo % 2^32 - C4_lo % 2^31) / 2^31) + T0_lo = XOR(D_lo, L03_lo) + T0_hi = XOR(D_hi, L03_hi) + T1_lo = XOR(D_lo, L08_lo) + T1_hi = XOR(D_hi, L08_hi) + T2_lo = XOR(D_lo, L13_lo) + T2_hi = XOR(D_hi, L13_hi) + T3_lo = XOR(D_lo, L18_lo) + T3_hi = XOR(D_hi, L18_hi) + T4_lo = XOR(D_lo, L23_lo) + T4_hi = XOR(D_hi, L23_hi) + L03_lo = (T2_lo % 2^32 - T2_lo % 2^21) / 2^21 + T2_hi * 2^11 + L03_hi = (T2_hi % 2^32 - T2_hi % 2^21) / 2^21 + T2_lo * 2^11 + L08_lo = (T4_lo % 2^32 - T4_lo % 2^3) / 2^3 + T4_hi * 2^29 % 2^32 + L08_hi = (T4_hi % 2^32 - T4_hi % 2^3) / 2^3 + T4_lo * 2^29 % 2^32 + L13_lo = T1_lo * 2^6 + (T1_hi % 2^32 - T1_hi % 2^26) / 2^26 + L13_hi = T1_hi * 2^6 + (T1_lo % 2^32 - T1_lo % 2^26) / 2^26 + L18_lo = T3_lo * 2^15 + (T3_hi % 2^32 - T3_hi % 2^17) / 2^17 + L18_hi = T3_hi * 2^15 + (T3_lo % 2^32 - T3_lo % 2^17) / 2^17 + L23_lo = (T0_lo % 2^32 - T0_lo % 2^2) / 2^2 + T0_hi * 2^30 % 2^32 + L23_hi = (T0_hi % 2^32 - T0_hi % 2^2) / 2^2 + T0_lo * 2^30 % 2^32 + D_lo = XOR(C3_lo, C5_lo * 2 + (C5_hi % 2^32 - C5_hi % 2^31) / 2^31) + D_hi = XOR(C3_hi, C5_hi * 2 + (C5_lo % 2^32 - C5_lo % 2^31) / 2^31) + T0_lo = XOR(D_lo, L04_lo) + T0_hi = XOR(D_hi, L04_hi) + T1_lo = XOR(D_lo, L09_lo) + T1_hi = XOR(D_hi, L09_hi) + T2_lo = XOR(D_lo, L14_lo) + T2_hi = XOR(D_hi, L14_hi) + T3_lo = XOR(D_lo, L19_lo) + T3_hi = XOR(D_hi, L19_hi) + T4_lo = XOR(D_lo, L24_lo) + T4_hi = XOR(D_hi, L24_hi) + L04_lo = T3_lo * 2^21 % 2^32 + (T3_hi % 2^32 - T3_hi % 2^11) / 2^11 + L04_hi = T3_hi * 2^21 % 2^32 + (T3_lo % 2^32 - T3_lo % 2^11) / 2^11 + L09_lo = T0_lo * 2^28 % 2^32 + (T0_hi % 2^32 - T0_hi % 2^4) / 2^4 + L09_hi = T0_hi * 2^28 % 2^32 + (T0_lo % 2^32 - T0_lo % 2^4) / 2^4 + L14_lo = T2_lo * 2^25 % 2^32 + (T2_hi % 2^32 - T2_hi % 2^7) / 2^7 + L14_hi = T2_hi * 2^25 % 2^32 + (T2_lo % 2^32 - T2_lo % 2^7) / 2^7 + L19_lo = (T4_lo % 2^32 - T4_lo % 2^8) / 2^8 + T4_hi * 2^24 % 2^32 + L19_hi = (T4_hi % 2^32 - T4_hi % 2^8) / 2^8 + T4_lo * 2^24 % 2^32 + L24_lo = (T1_lo % 2^32 - T1_lo % 2^9) / 2^9 + T1_hi * 2^23 % 2^32 + L24_hi = (T1_hi % 2^32 - T1_hi % 2^9) / 2^9 + T1_lo * 2^23 % 2^32 + D_lo = XOR(C4_lo, C1_lo * 2 + (C1_hi % 2^32 - C1_hi % 2^31) / 2^31) + D_hi = XOR(C4_hi, C1_hi * 2 + (C1_lo % 2^32 - C1_lo % 2^31) / 2^31) + T0_lo = XOR(D_lo, L05_lo) + T0_hi = XOR(D_hi, L05_hi) + T1_lo = XOR(D_lo, L10_lo) + T1_hi = XOR(D_hi, L10_hi) + T2_lo = XOR(D_lo, L15_lo) + T2_hi = XOR(D_hi, L15_hi) + T3_lo = XOR(D_lo, L20_lo) + T3_hi = XOR(D_hi, L20_hi) + T4_lo = XOR(D_lo, L25_lo) + T4_hi = XOR(D_hi, L25_hi) + L05_lo = T4_lo * 2^14 + (T4_hi % 2^32 - T4_hi % 2^18) / 2^18 + L05_hi = T4_hi * 2^14 + (T4_lo % 2^32 - T4_lo % 2^18) / 2^18 + L10_lo = T1_lo * 2^20 % 2^32 + (T1_hi % 2^32 - T1_hi % 2^12) / 2^12 + L10_hi = T1_hi * 2^20 % 2^32 + (T1_lo % 2^32 - T1_lo % 2^12) / 2^12 + L15_lo = T3_lo * 2^8 + (T3_hi % 2^32 - T3_hi % 2^24) / 2^24 + L15_hi = T3_hi * 2^8 + (T3_lo % 2^32 - T3_lo % 2^24) / 2^24 + L20_lo = T0_lo * 2^27 % 2^32 + (T0_hi % 2^32 - T0_hi % 2^5) / 2^5 + L20_hi = T0_hi * 2^27 % 2^32 + (T0_lo % 2^32 - T0_lo % 2^5) / 2^5 + L25_lo = (T2_lo % 2^32 - T2_lo % 2^25) / 2^25 + T2_hi * 2^7 + L25_hi = (T2_hi % 2^32 - T2_hi % 2^25) / 2^25 + T2_lo * 2^7 + D_lo = XOR(C5_lo, C2_lo * 2 + (C2_hi % 2^32 - C2_hi % 2^31) / 2^31) + D_hi = XOR(C5_hi, C2_hi * 2 + (C2_lo % 2^32 - C2_lo % 2^31) / 2^31) + T1_lo = XOR(D_lo, L06_lo) + T1_hi = XOR(D_hi, L06_hi) + T2_lo = XOR(D_lo, L11_lo) + T2_hi = XOR(D_hi, L11_hi) + T3_lo = XOR(D_lo, L16_lo) + T3_hi = XOR(D_hi, L16_hi) + T4_lo = XOR(D_lo, L21_lo) + T4_hi = XOR(D_hi, L21_hi) + L06_lo = T2_lo * 2^3 + (T2_hi % 2^32 - T2_hi % 2^29) / 2^29 + L06_hi = T2_hi * 2^3 + (T2_lo % 2^32 - T2_lo % 2^29) / 2^29 + L11_lo = T4_lo * 2^18 + (T4_hi % 2^32 - T4_hi % 2^14) / 2^14 + L11_hi = T4_hi * 2^18 + (T4_lo % 2^32 - T4_lo % 2^14) / 2^14 + L16_lo = (T1_lo % 2^32 - T1_lo % 2^28) / 2^28 + T1_hi * 2^4 + L16_hi = (T1_hi % 2^32 - T1_hi % 2^28) / 2^28 + T1_lo * 2^4 + L21_lo = (T3_lo % 2^32 - T3_lo % 2^23) / 2^23 + T3_hi * 2^9 + L21_hi = (T3_hi % 2^32 - T3_hi % 2^23) / 2^23 + T3_lo * 2^9 + L01_lo = XOR(D_lo, L01_lo) + L01_hi = XOR(D_hi, L01_hi) + L01_lo, L02_lo, L03_lo, L04_lo, L05_lo = XOR(L01_lo, AND(-1-L02_lo, L03_lo)), XOR(L02_lo, AND(-1-L03_lo, L04_lo)), XOR(L03_lo, AND(-1-L04_lo, L05_lo)), XOR(L04_lo, AND(-1-L05_lo, L01_lo)), XOR(L05_lo, AND(-1-L01_lo, L02_lo)) + L01_hi, L02_hi, L03_hi, L04_hi, L05_hi = XOR(L01_hi, AND(-1-L02_hi, L03_hi)), XOR(L02_hi, AND(-1-L03_hi, L04_hi)), XOR(L03_hi, AND(-1-L04_hi, L05_hi)), XOR(L04_hi, AND(-1-L05_hi, L01_hi)), XOR(L05_hi, AND(-1-L01_hi, L02_hi)) + L06_lo, L07_lo, L08_lo, L09_lo, L10_lo = XOR(L09_lo, AND(-1-L10_lo, L06_lo)), XOR(L10_lo, AND(-1-L06_lo, L07_lo)), XOR(L06_lo, AND(-1-L07_lo, L08_lo)), XOR(L07_lo, AND(-1-L08_lo, L09_lo)), XOR(L08_lo, AND(-1-L09_lo, L10_lo)) + L06_hi, L07_hi, L08_hi, L09_hi, L10_hi = XOR(L09_hi, AND(-1-L10_hi, L06_hi)), XOR(L10_hi, AND(-1-L06_hi, L07_hi)), XOR(L06_hi, AND(-1-L07_hi, L08_hi)), XOR(L07_hi, AND(-1-L08_hi, L09_hi)), XOR(L08_hi, AND(-1-L09_hi, L10_hi)) + L11_lo, L12_lo, L13_lo, L14_lo, L15_lo = XOR(L12_lo, AND(-1-L13_lo, L14_lo)), XOR(L13_lo, AND(-1-L14_lo, L15_lo)), XOR(L14_lo, AND(-1-L15_lo, L11_lo)), XOR(L15_lo, AND(-1-L11_lo, L12_lo)), XOR(L11_lo, AND(-1-L12_lo, L13_lo)) + L11_hi, L12_hi, L13_hi, L14_hi, L15_hi = XOR(L12_hi, AND(-1-L13_hi, L14_hi)), XOR(L13_hi, AND(-1-L14_hi, L15_hi)), XOR(L14_hi, AND(-1-L15_hi, L11_hi)), XOR(L15_hi, AND(-1-L11_hi, L12_hi)), XOR(L11_hi, AND(-1-L12_hi, L13_hi)) + L16_lo, L17_lo, L18_lo, L19_lo, L20_lo = XOR(L20_lo, AND(-1-L16_lo, L17_lo)), XOR(L16_lo, AND(-1-L17_lo, L18_lo)), XOR(L17_lo, AND(-1-L18_lo, L19_lo)), XOR(L18_lo, AND(-1-L19_lo, L20_lo)), XOR(L19_lo, AND(-1-L20_lo, L16_lo)) + L16_hi, L17_hi, L18_hi, L19_hi, L20_hi = XOR(L20_hi, AND(-1-L16_hi, L17_hi)), XOR(L16_hi, AND(-1-L17_hi, L18_hi)), XOR(L17_hi, AND(-1-L18_hi, L19_hi)), XOR(L18_hi, AND(-1-L19_hi, L20_hi)), XOR(L19_hi, AND(-1-L20_hi, L16_hi)) + L21_lo, L22_lo, L23_lo, L24_lo, L25_lo = XOR(L23_lo, AND(-1-L24_lo, L25_lo)), XOR(L24_lo, AND(-1-L25_lo, L21_lo)), XOR(L25_lo, AND(-1-L21_lo, L22_lo)), XOR(L21_lo, AND(-1-L22_lo, L23_lo)), XOR(L22_lo, AND(-1-L23_lo, L24_lo)) + L21_hi, L22_hi, L23_hi, L24_hi, L25_hi = XOR(L23_hi, AND(-1-L24_hi, L25_hi)), XOR(L24_hi, AND(-1-L25_hi, L21_hi)), XOR(L25_hi, AND(-1-L21_hi, L22_hi)), XOR(L21_hi, AND(-1-L22_hi, L23_hi)), XOR(L22_hi, AND(-1-L23_hi, L24_hi)) + L01_lo = XOR(L01_lo, RC_lo[round_idx]) + L01_hi = L01_hi + RC_hi[round_idx] -- RC_hi[] is either 0 or 0x80000000, so we could use fast addition instead of slow XOR + end + lanes_lo[1] = L01_lo; lanes_hi[1] = L01_hi + lanes_lo[2] = L02_lo; lanes_hi[2] = L02_hi + lanes_lo[3] = L03_lo; lanes_hi[3] = L03_hi + lanes_lo[4] = L04_lo; lanes_hi[4] = L04_hi + lanes_lo[5] = L05_lo; lanes_hi[5] = L05_hi + lanes_lo[6] = L06_lo; lanes_hi[6] = L06_hi + lanes_lo[7] = L07_lo; lanes_hi[7] = L07_hi + lanes_lo[8] = L08_lo; lanes_hi[8] = L08_hi + lanes_lo[9] = L09_lo; lanes_hi[9] = L09_hi + lanes_lo[10] = L10_lo; lanes_hi[10] = L10_hi + lanes_lo[11] = L11_lo; lanes_hi[11] = L11_hi + lanes_lo[12] = L12_lo; lanes_hi[12] = L12_hi + lanes_lo[13] = L13_lo; lanes_hi[13] = L13_hi + lanes_lo[14] = L14_lo; lanes_hi[14] = L14_hi + lanes_lo[15] = L15_lo; lanes_hi[15] = L15_hi + lanes_lo[16] = L16_lo; lanes_hi[16] = L16_hi + lanes_lo[17] = L17_lo; lanes_hi[17] = L17_hi + lanes_lo[18] = L18_lo; lanes_hi[18] = L18_hi + lanes_lo[19] = L19_lo; lanes_hi[19] = L19_hi + lanes_lo[20] = L20_lo; lanes_hi[20] = L20_hi + lanes_lo[21] = L21_lo; lanes_hi[21] = L21_hi + lanes_lo[22] = L22_lo; lanes_hi[22] = L22_hi + lanes_lo[23] = L23_lo; lanes_hi[23] = L23_hi + lanes_lo[24] = L24_lo; lanes_hi[24] = L24_hi + lanes_lo[25] = L25_lo; lanes_hi[25] = L25_hi + end + end + + + function blake2s_feed_64(H, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for pos = offs, offs + size - 1, 64 do + if str then + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + end + local v0, v1, v2, v3, v4, v5, v6, v7 = h1, h2, h3, h4, h5, h6, h7, h8 + local v8, v9, vA, vB, vC, vD, vE, vF = sha2_H_hi[1], sha2_H_hi[2], sha2_H_hi[3], sha2_H_hi[4], sha2_H_hi[5], sha2_H_hi[6], sha2_H_hi[7], sha2_H_hi[8] + bytes_compressed = bytes_compressed + (last_block_size or 64) + local t0 = bytes_compressed % 2^32 + local t1 = (bytes_compressed - t0) / 2^32 + vC = XOR(vC, t0) -- t0 = low_4_bytes(bytes_compressed) + vD = XOR(vD, t1) -- t1 = high_4_bytes(bytes_compressed) + if last_block_size then -- flag f0 + vE = -1 - vE + end + if is_last_node then -- flag f1 + vF = -1 - vF + end + for j = 1, 10 do + local row = sigma[j] + v0 = v0 + v4 + W[row[1]] + vC = XOR(vC, v0) % 2^32 / 2^16 + vC = vC % 1 * (2^32 - 1) + vC + v8 = v8 + vC + v4 = XOR(v4, v8) % 2^32 / 2^12 + v4 = v4 % 1 * (2^32 - 1) + v4 + v0 = v0 + v4 + W[row[2]] + vC = XOR(vC, v0) % 2^32 / 2^8 + vC = vC % 1 * (2^32 - 1) + vC + v8 = v8 + vC + v4 = XOR(v4, v8) % 2^32 / 2^7 + v4 = v4 % 1 * (2^32 - 1) + v4 + v1 = v1 + v5 + W[row[3]] + vD = XOR(vD, v1) % 2^32 / 2^16 + vD = vD % 1 * (2^32 - 1) + vD + v9 = v9 + vD + v5 = XOR(v5, v9) % 2^32 / 2^12 + v5 = v5 % 1 * (2^32 - 1) + v5 + v1 = v1 + v5 + W[row[4]] + vD = XOR(vD, v1) % 2^32 / 2^8 + vD = vD % 1 * (2^32 - 1) + vD + v9 = v9 + vD + v5 = XOR(v5, v9) % 2^32 / 2^7 + v5 = v5 % 1 * (2^32 - 1) + v5 + v2 = v2 + v6 + W[row[5]] + vE = XOR(vE, v2) % 2^32 / 2^16 + vE = vE % 1 * (2^32 - 1) + vE + vA = vA + vE + v6 = XOR(v6, vA) % 2^32 / 2^12 + v6 = v6 % 1 * (2^32 - 1) + v6 + v2 = v2 + v6 + W[row[6]] + vE = XOR(vE, v2) % 2^32 / 2^8 + vE = vE % 1 * (2^32 - 1) + vE + vA = vA + vE + v6 = XOR(v6, vA) % 2^32 / 2^7 + v6 = v6 % 1 * (2^32 - 1) + v6 + v3 = v3 + v7 + W[row[7]] + vF = XOR(vF, v3) % 2^32 / 2^16 + vF = vF % 1 * (2^32 - 1) + vF + vB = vB + vF + v7 = XOR(v7, vB) % 2^32 / 2^12 + v7 = v7 % 1 * (2^32 - 1) + v7 + v3 = v3 + v7 + W[row[8]] + vF = XOR(vF, v3) % 2^32 / 2^8 + vF = vF % 1 * (2^32 - 1) + vF + vB = vB + vF + v7 = XOR(v7, vB) % 2^32 / 2^7 + v7 = v7 % 1 * (2^32 - 1) + v7 + v0 = v0 + v5 + W[row[9]] + vF = XOR(vF, v0) % 2^32 / 2^16 + vF = vF % 1 * (2^32 - 1) + vF + vA = vA + vF + v5 = XOR(v5, vA) % 2^32 / 2^12 + v5 = v5 % 1 * (2^32 - 1) + v5 + v0 = v0 + v5 + W[row[10]] + vF = XOR(vF, v0) % 2^32 / 2^8 + vF = vF % 1 * (2^32 - 1) + vF + vA = vA + vF + v5 = XOR(v5, vA) % 2^32 / 2^7 + v5 = v5 % 1 * (2^32 - 1) + v5 + v1 = v1 + v6 + W[row[11]] + vC = XOR(vC, v1) % 2^32 / 2^16 + vC = vC % 1 * (2^32 - 1) + vC + vB = vB + vC + v6 = XOR(v6, vB) % 2^32 / 2^12 + v6 = v6 % 1 * (2^32 - 1) + v6 + v1 = v1 + v6 + W[row[12]] + vC = XOR(vC, v1) % 2^32 / 2^8 + vC = vC % 1 * (2^32 - 1) + vC + vB = vB + vC + v6 = XOR(v6, vB) % 2^32 / 2^7 + v6 = v6 % 1 * (2^32 - 1) + v6 + v2 = v2 + v7 + W[row[13]] + vD = XOR(vD, v2) % 2^32 / 2^16 + vD = vD % 1 * (2^32 - 1) + vD + v8 = v8 + vD + v7 = XOR(v7, v8) % 2^32 / 2^12 + v7 = v7 % 1 * (2^32 - 1) + v7 + v2 = v2 + v7 + W[row[14]] + vD = XOR(vD, v2) % 2^32 / 2^8 + vD = vD % 1 * (2^32 - 1) + vD + v8 = v8 + vD + v7 = XOR(v7, v8) % 2^32 / 2^7 + v7 = v7 % 1 * (2^32 - 1) + v7 + v3 = v3 + v4 + W[row[15]] + vE = XOR(vE, v3) % 2^32 / 2^16 + vE = vE % 1 * (2^32 - 1) + vE + v9 = v9 + vE + v4 = XOR(v4, v9) % 2^32 / 2^12 + v4 = v4 % 1 * (2^32 - 1) + v4 + v3 = v3 + v4 + W[row[16]] + vE = XOR(vE, v3) % 2^32 / 2^8 + vE = vE % 1 * (2^32 - 1) + vE + v9 = v9 + vE + v4 = XOR(v4, v9) % 2^32 / 2^7 + v4 = v4 % 1 * (2^32 - 1) + v4 + end + h1 = XOR(h1, v0, v8) + h2 = XOR(h2, v1, v9) + h3 = XOR(h3, v2, vA) + h4 = XOR(h4, v3, vB) + h5 = XOR(h5, v4, vC) + h6 = XOR(h6, v5, vD) + h7 = XOR(h7, v6, vE) + h8 = XOR(h8, v7, vF) + end + H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] = h1, h2, h3, h4, h5, h6, h7, h8 + return bytes_compressed + end + + + function blake2b_feed_128(H_lo, H_hi, str, offs, size, bytes_compressed, last_block_size, is_last_node) + -- offs >= 0, size >= 0, size is multiple of 128 + local W = common_W + local h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo = H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] + local h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi = H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] + for pos = offs, offs + size - 1, 128 do + if str then + for j = 1, 32 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + end + local v0_lo, v1_lo, v2_lo, v3_lo, v4_lo, v5_lo, v6_lo, v7_lo = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + local v0_hi, v1_hi, v2_hi, v3_hi, v4_hi, v5_hi, v6_hi, v7_hi = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + local v8_lo, v9_lo, vA_lo, vB_lo, vC_lo, vD_lo, vE_lo, vF_lo = sha2_H_lo[1], sha2_H_lo[2], sha2_H_lo[3], sha2_H_lo[4], sha2_H_lo[5], sha2_H_lo[6], sha2_H_lo[7], sha2_H_lo[8] + local v8_hi, v9_hi, vA_hi, vB_hi, vC_hi, vD_hi, vE_hi, vF_hi = sha2_H_hi[1], sha2_H_hi[2], sha2_H_hi[3], sha2_H_hi[4], sha2_H_hi[5], sha2_H_hi[6], sha2_H_hi[7], sha2_H_hi[8] + bytes_compressed = bytes_compressed + (last_block_size or 128) + local t0_lo = bytes_compressed % 2^32 + local t0_hi = (bytes_compressed - t0_lo) / 2^32 + vC_lo = XOR(vC_lo, t0_lo) -- t0 = low_8_bytes(bytes_compressed) + vC_hi = XOR(vC_hi, t0_hi) + -- t1 = high_8_bytes(bytes_compressed) = 0, message length is always below 2^53 bytes + if last_block_size then -- flag f0 + vE_lo = -1 - vE_lo + vE_hi = -1 - vE_hi + end + if is_last_node then -- flag f1 + vF_lo = -1 - vF_lo + vF_hi = -1 - vF_hi + end + for j = 1, 12 do + local row = sigma[j] + local k = row[1] * 2 + local z = v0_lo % 2^32 + v4_lo % 2^32 + W[k-1] + v0_lo = z % 2^32 + v0_hi = v0_hi + v4_hi + (z - v0_lo) / 2^32 + W[k] + vC_lo, vC_hi = XOR(vC_hi, v0_hi), XOR(vC_lo, v0_lo) + z = v8_lo % 2^32 + vC_lo % 2^32 + v8_lo = z % 2^32 + v8_hi = v8_hi + vC_hi + (z - v8_lo) / 2^32 + v4_lo, v4_hi = XOR(v4_lo, v8_lo), XOR(v4_hi, v8_hi) + local z_lo, z_hi = v4_lo % 2^24, v4_hi % 2^24 + v4_lo, v4_hi = (v4_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v4_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[2] * 2 + z = v0_lo % 2^32 + v4_lo % 2^32 + W[k-1] + v0_lo = z % 2^32 + v0_hi = v0_hi + v4_hi + (z - v0_lo) / 2^32 + W[k] + vC_lo, vC_hi = XOR(vC_lo, v0_lo), XOR(vC_hi, v0_hi) + z_lo, z_hi = vC_lo % 2^16, vC_hi % 2^16 + vC_lo, vC_hi = (vC_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vC_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = v8_lo % 2^32 + vC_lo % 2^32 + v8_lo = z % 2^32 + v8_hi = v8_hi + vC_hi + (z - v8_lo) / 2^32 + v4_lo, v4_hi = XOR(v4_lo, v8_lo), XOR(v4_hi, v8_hi) + z_lo, z_hi = v4_lo % 2^31, v4_hi % 2^31 + v4_lo, v4_hi = z_lo * 2^1 + (v4_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v4_lo - z_lo) / 2^31 % 2^1 + k = row[3] * 2 + z = v1_lo % 2^32 + v5_lo % 2^32 + W[k-1] + v1_lo = z % 2^32 + v1_hi = v1_hi + v5_hi + (z - v1_lo) / 2^32 + W[k] + vD_lo, vD_hi = XOR(vD_hi, v1_hi), XOR(vD_lo, v1_lo) + z = v9_lo % 2^32 + vD_lo % 2^32 + v9_lo = z % 2^32 + v9_hi = v9_hi + vD_hi + (z - v9_lo) / 2^32 + v5_lo, v5_hi = XOR(v5_lo, v9_lo), XOR(v5_hi, v9_hi) + z_lo, z_hi = v5_lo % 2^24, v5_hi % 2^24 + v5_lo, v5_hi = (v5_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v5_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[4] * 2 + z = v1_lo % 2^32 + v5_lo % 2^32 + W[k-1] + v1_lo = z % 2^32 + v1_hi = v1_hi + v5_hi + (z - v1_lo) / 2^32 + W[k] + vD_lo, vD_hi = XOR(vD_lo, v1_lo), XOR(vD_hi, v1_hi) + z_lo, z_hi = vD_lo % 2^16, vD_hi % 2^16 + vD_lo, vD_hi = (vD_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vD_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = v9_lo % 2^32 + vD_lo % 2^32 + v9_lo = z % 2^32 + v9_hi = v9_hi + vD_hi + (z - v9_lo) / 2^32 + v5_lo, v5_hi = XOR(v5_lo, v9_lo), XOR(v5_hi, v9_hi) + z_lo, z_hi = v5_lo % 2^31, v5_hi % 2^31 + v5_lo, v5_hi = z_lo * 2^1 + (v5_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v5_lo - z_lo) / 2^31 % 2^1 + k = row[5] * 2 + z = v2_lo % 2^32 + v6_lo % 2^32 + W[k-1] + v2_lo = z % 2^32 + v2_hi = v2_hi + v6_hi + (z - v2_lo) / 2^32 + W[k] + vE_lo, vE_hi = XOR(vE_hi, v2_hi), XOR(vE_lo, v2_lo) + z = vA_lo % 2^32 + vE_lo % 2^32 + vA_lo = z % 2^32 + vA_hi = vA_hi + vE_hi + (z - vA_lo) / 2^32 + v6_lo, v6_hi = XOR(v6_lo, vA_lo), XOR(v6_hi, vA_hi) + z_lo, z_hi = v6_lo % 2^24, v6_hi % 2^24 + v6_lo, v6_hi = (v6_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v6_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[6] * 2 + z = v2_lo % 2^32 + v6_lo % 2^32 + W[k-1] + v2_lo = z % 2^32 + v2_hi = v2_hi + v6_hi + (z - v2_lo) / 2^32 + W[k] + vE_lo, vE_hi = XOR(vE_lo, v2_lo), XOR(vE_hi, v2_hi) + z_lo, z_hi = vE_lo % 2^16, vE_hi % 2^16 + vE_lo, vE_hi = (vE_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vE_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = vA_lo % 2^32 + vE_lo % 2^32 + vA_lo = z % 2^32 + vA_hi = vA_hi + vE_hi + (z - vA_lo) / 2^32 + v6_lo, v6_hi = XOR(v6_lo, vA_lo), XOR(v6_hi, vA_hi) + z_lo, z_hi = v6_lo % 2^31, v6_hi % 2^31 + v6_lo, v6_hi = z_lo * 2^1 + (v6_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v6_lo - z_lo) / 2^31 % 2^1 + k = row[7] * 2 + z = v3_lo % 2^32 + v7_lo % 2^32 + W[k-1] + v3_lo = z % 2^32 + v3_hi = v3_hi + v7_hi + (z - v3_lo) / 2^32 + W[k] + vF_lo, vF_hi = XOR(vF_hi, v3_hi), XOR(vF_lo, v3_lo) + z = vB_lo % 2^32 + vF_lo % 2^32 + vB_lo = z % 2^32 + vB_hi = vB_hi + vF_hi + (z - vB_lo) / 2^32 + v7_lo, v7_hi = XOR(v7_lo, vB_lo), XOR(v7_hi, vB_hi) + z_lo, z_hi = v7_lo % 2^24, v7_hi % 2^24 + v7_lo, v7_hi = (v7_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v7_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[8] * 2 + z = v3_lo % 2^32 + v7_lo % 2^32 + W[k-1] + v3_lo = z % 2^32 + v3_hi = v3_hi + v7_hi + (z - v3_lo) / 2^32 + W[k] + vF_lo, vF_hi = XOR(vF_lo, v3_lo), XOR(vF_hi, v3_hi) + z_lo, z_hi = vF_lo % 2^16, vF_hi % 2^16 + vF_lo, vF_hi = (vF_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vF_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = vB_lo % 2^32 + vF_lo % 2^32 + vB_lo = z % 2^32 + vB_hi = vB_hi + vF_hi + (z - vB_lo) / 2^32 + v7_lo, v7_hi = XOR(v7_lo, vB_lo), XOR(v7_hi, vB_hi) + z_lo, z_hi = v7_lo % 2^31, v7_hi % 2^31 + v7_lo, v7_hi = z_lo * 2^1 + (v7_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v7_lo - z_lo) / 2^31 % 2^1 + k = row[9] * 2 + z = v0_lo % 2^32 + v5_lo % 2^32 + W[k-1] + v0_lo = z % 2^32 + v0_hi = v0_hi + v5_hi + (z - v0_lo) / 2^32 + W[k] + vF_lo, vF_hi = XOR(vF_hi, v0_hi), XOR(vF_lo, v0_lo) + z = vA_lo % 2^32 + vF_lo % 2^32 + vA_lo = z % 2^32 + vA_hi = vA_hi + vF_hi + (z - vA_lo) / 2^32 + v5_lo, v5_hi = XOR(v5_lo, vA_lo), XOR(v5_hi, vA_hi) + z_lo, z_hi = v5_lo % 2^24, v5_hi % 2^24 + v5_lo, v5_hi = (v5_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v5_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[10] * 2 + z = v0_lo % 2^32 + v5_lo % 2^32 + W[k-1] + v0_lo = z % 2^32 + v0_hi = v0_hi + v5_hi + (z - v0_lo) / 2^32 + W[k] + vF_lo, vF_hi = XOR(vF_lo, v0_lo), XOR(vF_hi, v0_hi) + z_lo, z_hi = vF_lo % 2^16, vF_hi % 2^16 + vF_lo, vF_hi = (vF_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vF_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = vA_lo % 2^32 + vF_lo % 2^32 + vA_lo = z % 2^32 + vA_hi = vA_hi + vF_hi + (z - vA_lo) / 2^32 + v5_lo, v5_hi = XOR(v5_lo, vA_lo), XOR(v5_hi, vA_hi) + z_lo, z_hi = v5_lo % 2^31, v5_hi % 2^31 + v5_lo, v5_hi = z_lo * 2^1 + (v5_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v5_lo - z_lo) / 2^31 % 2^1 + k = row[11] * 2 + z = v1_lo % 2^32 + v6_lo % 2^32 + W[k-1] + v1_lo = z % 2^32 + v1_hi = v1_hi + v6_hi + (z - v1_lo) / 2^32 + W[k] + vC_lo, vC_hi = XOR(vC_hi, v1_hi), XOR(vC_lo, v1_lo) + z = vB_lo % 2^32 + vC_lo % 2^32 + vB_lo = z % 2^32 + vB_hi = vB_hi + vC_hi + (z - vB_lo) / 2^32 + v6_lo, v6_hi = XOR(v6_lo, vB_lo), XOR(v6_hi, vB_hi) + z_lo, z_hi = v6_lo % 2^24, v6_hi % 2^24 + v6_lo, v6_hi = (v6_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v6_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[12] * 2 + z = v1_lo % 2^32 + v6_lo % 2^32 + W[k-1] + v1_lo = z % 2^32 + v1_hi = v1_hi + v6_hi + (z - v1_lo) / 2^32 + W[k] + vC_lo, vC_hi = XOR(vC_lo, v1_lo), XOR(vC_hi, v1_hi) + z_lo, z_hi = vC_lo % 2^16, vC_hi % 2^16 + vC_lo, vC_hi = (vC_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vC_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = vB_lo % 2^32 + vC_lo % 2^32 + vB_lo = z % 2^32 + vB_hi = vB_hi + vC_hi + (z - vB_lo) / 2^32 + v6_lo, v6_hi = XOR(v6_lo, vB_lo), XOR(v6_hi, vB_hi) + z_lo, z_hi = v6_lo % 2^31, v6_hi % 2^31 + v6_lo, v6_hi = z_lo * 2^1 + (v6_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v6_lo - z_lo) / 2^31 % 2^1 + k = row[13] * 2 + z = v2_lo % 2^32 + v7_lo % 2^32 + W[k-1] + v2_lo = z % 2^32 + v2_hi = v2_hi + v7_hi + (z - v2_lo) / 2^32 + W[k] + vD_lo, vD_hi = XOR(vD_hi, v2_hi), XOR(vD_lo, v2_lo) + z = v8_lo % 2^32 + vD_lo % 2^32 + v8_lo = z % 2^32 + v8_hi = v8_hi + vD_hi + (z - v8_lo) / 2^32 + v7_lo, v7_hi = XOR(v7_lo, v8_lo), XOR(v7_hi, v8_hi) + z_lo, z_hi = v7_lo % 2^24, v7_hi % 2^24 + v7_lo, v7_hi = (v7_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v7_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[14] * 2 + z = v2_lo % 2^32 + v7_lo % 2^32 + W[k-1] + v2_lo = z % 2^32 + v2_hi = v2_hi + v7_hi + (z - v2_lo) / 2^32 + W[k] + vD_lo, vD_hi = XOR(vD_lo, v2_lo), XOR(vD_hi, v2_hi) + z_lo, z_hi = vD_lo % 2^16, vD_hi % 2^16 + vD_lo, vD_hi = (vD_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vD_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = v8_lo % 2^32 + vD_lo % 2^32 + v8_lo = z % 2^32 + v8_hi = v8_hi + vD_hi + (z - v8_lo) / 2^32 + v7_lo, v7_hi = XOR(v7_lo, v8_lo), XOR(v7_hi, v8_hi) + z_lo, z_hi = v7_lo % 2^31, v7_hi % 2^31 + v7_lo, v7_hi = z_lo * 2^1 + (v7_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v7_lo - z_lo) / 2^31 % 2^1 + k = row[15] * 2 + z = v3_lo % 2^32 + v4_lo % 2^32 + W[k-1] + v3_lo = z % 2^32 + v3_hi = v3_hi + v4_hi + (z - v3_lo) / 2^32 + W[k] + vE_lo, vE_hi = XOR(vE_hi, v3_hi), XOR(vE_lo, v3_lo) + z = v9_lo % 2^32 + vE_lo % 2^32 + v9_lo = z % 2^32 + v9_hi = v9_hi + vE_hi + (z - v9_lo) / 2^32 + v4_lo, v4_hi = XOR(v4_lo, v9_lo), XOR(v4_hi, v9_hi) + z_lo, z_hi = v4_lo % 2^24, v4_hi % 2^24 + v4_lo, v4_hi = (v4_lo - z_lo) / 2^24 % 2^8 + z_hi * 2^8, (v4_hi - z_hi) / 2^24 % 2^8 + z_lo * 2^8 + k = row[16] * 2 + z = v3_lo % 2^32 + v4_lo % 2^32 + W[k-1] + v3_lo = z % 2^32 + v3_hi = v3_hi + v4_hi + (z - v3_lo) / 2^32 + W[k] + vE_lo, vE_hi = XOR(vE_lo, v3_lo), XOR(vE_hi, v3_hi) + z_lo, z_hi = vE_lo % 2^16, vE_hi % 2^16 + vE_lo, vE_hi = (vE_lo - z_lo) / 2^16 % 2^16 + z_hi * 2^16, (vE_hi - z_hi) / 2^16 % 2^16 + z_lo * 2^16 + z = v9_lo % 2^32 + vE_lo % 2^32 + v9_lo = z % 2^32 + v9_hi = v9_hi + vE_hi + (z - v9_lo) / 2^32 + v4_lo, v4_hi = XOR(v4_lo, v9_lo), XOR(v4_hi, v9_hi) + z_lo, z_hi = v4_lo % 2^31, v4_hi % 2^31 + v4_lo, v4_hi = z_lo * 2^1 + (v4_hi - z_hi) / 2^31 % 2^1, z_hi * 2^1 + (v4_lo - z_lo) / 2^31 % 2^1 + end + h1_lo = XOR(h1_lo, v0_lo, v8_lo) % 2^32 + h2_lo = XOR(h2_lo, v1_lo, v9_lo) % 2^32 + h3_lo = XOR(h3_lo, v2_lo, vA_lo) % 2^32 + h4_lo = XOR(h4_lo, v3_lo, vB_lo) % 2^32 + h5_lo = XOR(h5_lo, v4_lo, vC_lo) % 2^32 + h6_lo = XOR(h6_lo, v5_lo, vD_lo) % 2^32 + h7_lo = XOR(h7_lo, v6_lo, vE_lo) % 2^32 + h8_lo = XOR(h8_lo, v7_lo, vF_lo) % 2^32 + h1_hi = XOR(h1_hi, v0_hi, v8_hi) % 2^32 + h2_hi = XOR(h2_hi, v1_hi, v9_hi) % 2^32 + h3_hi = XOR(h3_hi, v2_hi, vA_hi) % 2^32 + h4_hi = XOR(h4_hi, v3_hi, vB_hi) % 2^32 + h5_hi = XOR(h5_hi, v4_hi, vC_hi) % 2^32 + h6_hi = XOR(h6_hi, v5_hi, vD_hi) % 2^32 + h7_hi = XOR(h7_hi, v6_hi, vE_hi) % 2^32 + h8_hi = XOR(h8_hi, v7_hi, vF_hi) % 2^32 + end + H_lo[1], H_lo[2], H_lo[3], H_lo[4], H_lo[5], H_lo[6], H_lo[7], H_lo[8] = h1_lo, h2_lo, h3_lo, h4_lo, h5_lo, h6_lo, h7_lo, h8_lo + H_hi[1], H_hi[2], H_hi[3], H_hi[4], H_hi[5], H_hi[6], H_hi[7], H_hi[8] = h1_hi, h2_hi, h3_hi, h4_hi, h5_hi, h6_hi, h7_hi, h8_hi + return bytes_compressed + end + + + function blake3_feed_64(str, offs, size, flags, chunk_index, H_in, H_out, wide_output, block_length) + -- offs >= 0, size >= 0, size is multiple of 64 + block_length = block_length or 64 + local W = common_W + local h1, h2, h3, h4, h5, h6, h7, h8 = H_in[1], H_in[2], H_in[3], H_in[4], H_in[5], H_in[6], H_in[7], H_in[8] + H_out = H_out or H_in + for pos = offs, offs + size - 1, 64 do + if str then + for j = 1, 16 do + pos = pos + 4 + local a, b, c, d = byte(str, pos - 3, pos) + W[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + end + local v0, v1, v2, v3, v4, v5, v6, v7 = h1, h2, h3, h4, h5, h6, h7, h8 + local v8, v9, vA, vB = sha2_H_hi[1], sha2_H_hi[2], sha2_H_hi[3], sha2_H_hi[4] + local vC = chunk_index % 2^32 -- t0 = low_4_bytes(chunk_index) + local vD = (chunk_index - vC) / 2^32 -- t1 = high_4_bytes(chunk_index) + local vE, vF = block_length, flags + for j = 1, 7 do + v0 = v0 + v4 + W[perm_blake3[j]] + vC = XOR(vC, v0) % 2^32 / 2^16 + vC = vC % 1 * (2^32 - 1) + vC + v8 = v8 + vC + v4 = XOR(v4, v8) % 2^32 / 2^12 + v4 = v4 % 1 * (2^32 - 1) + v4 + v0 = v0 + v4 + W[perm_blake3[j + 14]] + vC = XOR(vC, v0) % 2^32 / 2^8 + vC = vC % 1 * (2^32 - 1) + vC + v8 = v8 + vC + v4 = XOR(v4, v8) % 2^32 / 2^7 + v4 = v4 % 1 * (2^32 - 1) + v4 + v1 = v1 + v5 + W[perm_blake3[j + 1]] + vD = XOR(vD, v1) % 2^32 / 2^16 + vD = vD % 1 * (2^32 - 1) + vD + v9 = v9 + vD + v5 = XOR(v5, v9) % 2^32 / 2^12 + v5 = v5 % 1 * (2^32 - 1) + v5 + v1 = v1 + v5 + W[perm_blake3[j + 2]] + vD = XOR(vD, v1) % 2^32 / 2^8 + vD = vD % 1 * (2^32 - 1) + vD + v9 = v9 + vD + v5 = XOR(v5, v9) % 2^32 / 2^7 + v5 = v5 % 1 * (2^32 - 1) + v5 + v2 = v2 + v6 + W[perm_blake3[j + 16]] + vE = XOR(vE, v2) % 2^32 / 2^16 + vE = vE % 1 * (2^32 - 1) + vE + vA = vA + vE + v6 = XOR(v6, vA) % 2^32 / 2^12 + v6 = v6 % 1 * (2^32 - 1) + v6 + v2 = v2 + v6 + W[perm_blake3[j + 7]] + vE = XOR(vE, v2) % 2^32 / 2^8 + vE = vE % 1 * (2^32 - 1) + vE + vA = vA + vE + v6 = XOR(v6, vA) % 2^32 / 2^7 + v6 = v6 % 1 * (2^32 - 1) + v6 + v3 = v3 + v7 + W[perm_blake3[j + 15]] + vF = XOR(vF, v3) % 2^32 / 2^16 + vF = vF % 1 * (2^32 - 1) + vF + vB = vB + vF + v7 = XOR(v7, vB) % 2^32 / 2^12 + v7 = v7 % 1 * (2^32 - 1) + v7 + v3 = v3 + v7 + W[perm_blake3[j + 17]] + vF = XOR(vF, v3) % 2^32 / 2^8 + vF = vF % 1 * (2^32 - 1) + vF + vB = vB + vF + v7 = XOR(v7, vB) % 2^32 / 2^7 + v7 = v7 % 1 * (2^32 - 1) + v7 + v0 = v0 + v5 + W[perm_blake3[j + 21]] + vF = XOR(vF, v0) % 2^32 / 2^16 + vF = vF % 1 * (2^32 - 1) + vF + vA = vA + vF + v5 = XOR(v5, vA) % 2^32 / 2^12 + v5 = v5 % 1 * (2^32 - 1) + v5 + v0 = v0 + v5 + W[perm_blake3[j + 5]] + vF = XOR(vF, v0) % 2^32 / 2^8 + vF = vF % 1 * (2^32 - 1) + vF + vA = vA + vF + v5 = XOR(v5, vA) % 2^32 / 2^7 + v5 = v5 % 1 * (2^32 - 1) + v5 + v1 = v1 + v6 + W[perm_blake3[j + 3]] + vC = XOR(vC, v1) % 2^32 / 2^16 + vC = vC % 1 * (2^32 - 1) + vC + vB = vB + vC + v6 = XOR(v6, vB) % 2^32 / 2^12 + v6 = v6 % 1 * (2^32 - 1) + v6 + v1 = v1 + v6 + W[perm_blake3[j + 6]] + vC = XOR(vC, v1) % 2^32 / 2^8 + vC = vC % 1 * (2^32 - 1) + vC + vB = vB + vC + v6 = XOR(v6, vB) % 2^32 / 2^7 + v6 = v6 % 1 * (2^32 - 1) + v6 + v2 = v2 + v7 + W[perm_blake3[j + 4]] + vD = XOR(vD, v2) % 2^32 / 2^16 + vD = vD % 1 * (2^32 - 1) + vD + v8 = v8 + vD + v7 = XOR(v7, v8) % 2^32 / 2^12 + v7 = v7 % 1 * (2^32 - 1) + v7 + v2 = v2 + v7 + W[perm_blake3[j + 18]] + vD = XOR(vD, v2) % 2^32 / 2^8 + vD = vD % 1 * (2^32 - 1) + vD + v8 = v8 + vD + v7 = XOR(v7, v8) % 2^32 / 2^7 + v7 = v7 % 1 * (2^32 - 1) + v7 + v3 = v3 + v4 + W[perm_blake3[j + 19]] + vE = XOR(vE, v3) % 2^32 / 2^16 + vE = vE % 1 * (2^32 - 1) + vE + v9 = v9 + vE + v4 = XOR(v4, v9) % 2^32 / 2^12 + v4 = v4 % 1 * (2^32 - 1) + v4 + v3 = v3 + v4 + W[perm_blake3[j + 20]] + vE = XOR(vE, v3) % 2^32 / 2^8 + vE = vE % 1 * (2^32 - 1) + vE + v9 = v9 + vE + v4 = XOR(v4, v9) % 2^32 / 2^7 + v4 = v4 % 1 * (2^32 - 1) + v4 + end + if wide_output then + H_out[ 9] = XOR(h1, v8) + H_out[10] = XOR(h2, v9) + H_out[11] = XOR(h3, vA) + H_out[12] = XOR(h4, vB) + H_out[13] = XOR(h5, vC) + H_out[14] = XOR(h6, vD) + H_out[15] = XOR(h7, vE) + H_out[16] = XOR(h8, vF) + end + h1 = XOR(v0, v8) + h2 = XOR(v1, v9) + h3 = XOR(v2, vA) + h4 = XOR(v3, vB) + h5 = XOR(v4, vC) + h6 = XOR(v5, vD) + h7 = XOR(v6, vE) + h8 = XOR(v7, vF) + end + H_out[1], H_out[2], H_out[3], H_out[4], H_out[5], H_out[6], H_out[7], H_out[8] = h1, h2, h3, h4, h5, h6, h7, h8 + end + +end + + +-------------------------------------------------------------------------------- +-- MAGIC NUMBERS CALCULATOR +-------------------------------------------------------------------------------- +-- Q: +-- Is 53-bit "double" math enough to calculate square roots and cube roots of primes with 64 correct bits after decimal point? +-- A: +-- Yes, 53-bit "double" arithmetic is enough. +-- We could obtain first 40 bits by direct calculation of p^(1/3) and next 40 bits by one step of Newton's method. + +do + local function mul(src1, src2, factor, result_length) + -- src1, src2 - long integers (arrays of digits in base 2^24) + -- factor - small integer + -- returns long integer result (src1 * src2 * factor) and its floating point approximation + local result, carry, value, weight = {}, 0.0, 0.0, 1.0 + for j = 1, result_length do + for k = math_max(1, j + 1 - #src2), math_min(j, #src1) do + carry = carry + factor * src1[k] * src2[j + 1 - k] -- "int32" is not enough for multiplication result, that's why "factor" must be of type "double" + end + local digit = carry % 2^24 + result[j] = floor(digit) + carry = (carry - digit) / 2^24 + value = value + digit * weight + weight = weight * 2^24 + end + return result, value + end + + local idx, step, p, one, sqrt_hi, sqrt_lo = 0, {4, 1, 2, -2, 2}, 4, {1}, sha2_H_hi, sha2_H_lo + repeat + p = p + step[p % 6] + local d = 1 + repeat + d = d + step[d % 6] + if d*d > p then -- next prime number is found + local root = p^(1/3) + local R = root * 2^40 + R = mul({R - R % 1}, one, 1.0, 2) + local _, delta = mul(R, mul(R, R, 1.0, 4), -1.0, 4) + local hi = R[2] % 65536 * 65536 + floor(R[1] / 256) + local lo = R[1] % 256 * 16777216 + floor(delta * (2^-56 / 3) * root / p) + if idx < 16 then + root = p^(1/2) + R = root * 2^40 + R = mul({R - R % 1}, one, 1.0, 2) + _, delta = mul(R, R, -1.0, 2) + local hi = R[2] % 65536 * 65536 + floor(R[1] / 256) + local lo = R[1] % 256 * 16777216 + floor(delta * 2^-17 / root) + local idx = idx % 8 + 1 + sha2_H_ext256[224][idx] = lo + sqrt_hi[idx], sqrt_lo[idx] = hi, lo + hi * hi_factor + if idx > 7 then + sqrt_hi, sqrt_lo = sha2_H_ext512_hi[384], sha2_H_ext512_lo[384] + end + end + idx = idx + 1 + sha2_K_hi[idx], sha2_K_lo[idx] = hi, lo % K_lo_modulo + hi * hi_factor + break + end + until p % d == 0 + until idx > 79 +end + +-- Calculating IVs for SHA512/224 and SHA512/256 +for width = 224, 256, 32 do + local H_lo, H_hi = {} + if HEX64 then + for j = 1, 8 do + H_lo[j] = XORA5(sha2_H_lo[j]) + end + else + H_hi = {} + for j = 1, 8 do + H_lo[j] = XORA5(sha2_H_lo[j]) + H_hi[j] = XORA5(sha2_H_hi[j]) + end + end + sha512_feed_128(H_lo, H_hi, "SHA-512/"..tostring(width).."\128"..string_rep("\0", 115).."\88", 0, 128) + sha2_H_ext512_lo[width] = H_lo + sha2_H_ext512_hi[width] = H_hi +end + +-- Constants for MD5 +do + local sin, abs, modf = math.sin, math.abs, math.modf + for idx = 1, 64 do + -- we can't use formula floor(abs(sin(idx))*2^32) because its result may be beyond integer range on Lua built with 32-bit integers + local hi, lo = modf(abs(sin(idx)) * 2^16) + md5_K[idx] = hi * 65536 + floor(lo * 2^16) + end +end + +-- Constants for SHA-3 +do + local sh_reg = 29 + + local function next_bit() + local r = sh_reg % 2 + sh_reg = XOR_BYTE((sh_reg - r) / 2, 142 * r) + return r + end + + for idx = 1, 24 do + local lo, m = 0 + for _ = 1, 6 do + m = m and m * m * 2 or 1 + lo = lo + next_bit() * m + end + local hi = next_bit() * m + sha3_RC_hi[idx], sha3_RC_lo[idx] = hi, lo + hi * hi_factor_keccak + end +end + +if branch == "FFI" then + sha2_K_hi = ffi.new("uint32_t[?]", #sha2_K_hi + 1, 0, unpack(sha2_K_hi)) + sha2_K_lo = ffi.new("int64_t[?]", #sha2_K_lo + 1, 0, unpack(sha2_K_lo)) + --md5_K = ffi.new("uint32_t[?]", #md5_K + 1, 0, unpack(md5_K)) + if hi_factor_keccak == 0 then + sha3_RC_lo = ffi.new("uint32_t[?]", #sha3_RC_lo + 1, 0, unpack(sha3_RC_lo)) + sha3_RC_hi = ffi.new("uint32_t[?]", #sha3_RC_hi + 1, 0, unpack(sha3_RC_hi)) + else + sha3_RC_lo = ffi.new("int64_t[?]", #sha3_RC_lo + 1, 0, unpack(sha3_RC_lo)) + end +end + + +-------------------------------------------------------------------------------- +-- MAIN FUNCTIONS +-------------------------------------------------------------------------------- + +local function sha256ext(width, message) + -- Create an instance (private objects for current calculation) + local H, length, tail = {unpack(sha2_H_ext256[width])}, 0.0, "" + + local function partial(message_part) + if message_part then + if tail then + length = length + #message_part + local offs = 0 + if tail ~= "" and #tail + #message_part >= 64 then + offs = 64 - #tail + sha256_feed_64(H, tail..sub(message_part, 1, offs), 0, 64) + tail = "" + end + local size = #message_part - offs + local size_tail = size % 64 + sha256_feed_64(H, message_part, offs, size - size_tail) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + local final_blocks = {tail, "\128", string_rep("\0", (-9 - length) % 64 + 1)} + tail = nil + -- Assuming user data length is shorter than (2^53)-9 bytes + -- Anyway, it looks very unrealistic that someone would spend more than a year of calculations to process 2^53 bytes of data by using this Lua script :-) + -- 2^53 bytes = 2^56 bits, so "bit-counter" fits in 7 bytes + length = length * (8 / 256^7) -- convert "byte-counter" to "bit-counter" and move decimal point to the left + for j = 4, 10 do + length = length % 1 * 256 + final_blocks[j] = char(floor(length)) + end + final_blocks = table_concat(final_blocks) + sha256_feed_64(H, final_blocks, 0, #final_blocks) + local max_reg = width / 32 + for j = 1, max_reg do + H[j] = HEX(H[j]) + end + H = table_concat(H, "", 1, max_reg) + end + return H + end + end + + if message then + -- Actually perform calculations and return the SHA256 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get SHA256 digest by invoking this function without an argument + return partial + end +end + + +local function sha512ext(width, message) + -- Create an instance (private objects for current calculation) + local length, tail, H_lo, H_hi = 0.0, "", {unpack(sha2_H_ext512_lo[width])}, not HEX64 and {unpack(sha2_H_ext512_hi[width])} + + local function partial(message_part) + if message_part then + if tail then + length = length + #message_part + local offs = 0 + if tail ~= "" and #tail + #message_part >= 128 then + offs = 128 - #tail + sha512_feed_128(H_lo, H_hi, tail..sub(message_part, 1, offs), 0, 128) + tail = "" + end + local size = #message_part - offs + local size_tail = size % 128 + sha512_feed_128(H_lo, H_hi, message_part, offs, size - size_tail) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + local final_blocks = {tail, "\128", string_rep("\0", (-17-length) % 128 + 9)} + tail = nil + -- Assuming user data length is shorter than (2^53)-17 bytes + -- 2^53 bytes = 2^56 bits, so "bit-counter" fits in 7 bytes + length = length * (8 / 256^7) -- convert "byte-counter" to "bit-counter" and move floating point to the left + for j = 4, 10 do + length = length % 1 * 256 + final_blocks[j] = char(floor(length)) + end + final_blocks = table_concat(final_blocks) + sha512_feed_128(H_lo, H_hi, final_blocks, 0, #final_blocks) + local max_reg = ceil(width / 64) + if HEX64 then + for j = 1, max_reg do + H_lo[j] = HEX64(H_lo[j]) + end + else + for j = 1, max_reg do + H_lo[j] = HEX(H_hi[j])..HEX(H_lo[j]) + end + H_hi = nil + end + H_lo = sub(table_concat(H_lo, "", 1, max_reg), 1, width / 4) + end + return H_lo + end + end + + if message then + -- Actually perform calculations and return the SHA512 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get SHA512 digest by invoking this function without an argument + return partial + end +end + + +local function md5(message) + -- Create an instance (private objects for current calculation) + local H, length, tail = {unpack(md5_sha1_H, 1, 4)}, 0.0, "" + + local function partial(message_part) + if message_part then + if tail then + length = length + #message_part + local offs = 0 + if tail ~= "" and #tail + #message_part >= 64 then + offs = 64 - #tail + md5_feed_64(H, tail..sub(message_part, 1, offs), 0, 64) + tail = "" + end + local size = #message_part - offs + local size_tail = size % 64 + md5_feed_64(H, message_part, offs, size - size_tail) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + local final_blocks = {tail, "\128", string_rep("\0", (-9 - length) % 64)} + tail = nil + length = length * 8 -- convert "byte-counter" to "bit-counter" + for j = 4, 11 do + local low_byte = length % 256 + final_blocks[j] = char(low_byte) + length = (length - low_byte) / 256 + end + final_blocks = table_concat(final_blocks) + md5_feed_64(H, final_blocks, 0, #final_blocks) + for j = 1, 4 do + H[j] = HEX(H[j]) + end + H = gsub(table_concat(H), "(..)(..)(..)(..)", "%4%3%2%1") + end + return H + end + end + + if message then + -- Actually perform calculations and return the MD5 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get MD5 digest by invoking this function without an argument + return partial + end +end + + +local function sha1(message) + -- Create an instance (private objects for current calculation) + local H, length, tail = {unpack(md5_sha1_H)}, 0.0, "" + + local function partial(message_part) + if message_part then + if tail then + length = length + #message_part + local offs = 0 + if tail ~= "" and #tail + #message_part >= 64 then + offs = 64 - #tail + sha1_feed_64(H, tail..sub(message_part, 1, offs), 0, 64) + tail = "" + end + local size = #message_part - offs + local size_tail = size % 64 + sha1_feed_64(H, message_part, offs, size - size_tail) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + local final_blocks = {tail, "\128", string_rep("\0", (-9 - length) % 64 + 1)} + tail = nil + -- Assuming user data length is shorter than (2^53)-9 bytes + -- 2^53 bytes = 2^56 bits, so "bit-counter" fits in 7 bytes + length = length * (8 / 256^7) -- convert "byte-counter" to "bit-counter" and move decimal point to the left + for j = 4, 10 do + length = length % 1 * 256 + final_blocks[j] = char(floor(length)) + end + final_blocks = table_concat(final_blocks) + sha1_feed_64(H, final_blocks, 0, #final_blocks) + for j = 1, 5 do + H[j] = HEX(H[j]) + end + H = table_concat(H) + end + return H + end + end + + if message then + -- Actually perform calculations and return the SHA-1 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get SHA-1 digest by invoking this function without an argument + return partial + end +end + + +local function keccak(block_size_in_bytes, digest_size_in_bytes, is_SHAKE, message) + -- "block_size_in_bytes" is multiple of 8 + if type(digest_size_in_bytes) ~= "number" then + -- arguments in SHAKE are swapped: + -- NIST FIPS 202 defines SHAKE(message,num_bits) + -- this module defines SHAKE(num_bytes,message) + -- it's easy to forget about this swap, hence the check + error("Argument 'digest_size_in_bytes' must be a number", 2) + end + -- Create an instance (private objects for current calculation) + local tail, lanes_lo, lanes_hi = "", create_array_of_lanes(), hi_factor_keccak == 0 and create_array_of_lanes() + local result + + local function partial(message_part) + if message_part then + if tail then + local offs = 0 + if tail ~= "" and #tail + #message_part >= block_size_in_bytes then + offs = block_size_in_bytes - #tail + keccak_feed(lanes_lo, lanes_hi, tail..sub(message_part, 1, offs), 0, block_size_in_bytes, block_size_in_bytes) + tail = "" + end + local size = #message_part - offs + local size_tail = size % block_size_in_bytes + keccak_feed(lanes_lo, lanes_hi, message_part, offs, size - size_tail, block_size_in_bytes) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + -- append the following bits to the message: for usual SHA-3: 011(0*)1, for SHAKE: 11111(0*)1 + local gap_start = is_SHAKE and 31 or 6 + tail = tail..(#tail + 1 == block_size_in_bytes and char(gap_start + 128) or char(gap_start)..string_rep("\0", (-2 - #tail) % block_size_in_bytes).."\128") + keccak_feed(lanes_lo, lanes_hi, tail, 0, #tail, block_size_in_bytes) + tail = nil + local lanes_used = 0 + local total_lanes = floor(block_size_in_bytes / 8) + local qwords = {} + + local function get_next_qwords_of_digest(qwords_qty) + -- returns not more than 'qwords_qty' qwords ('qwords_qty' might be non-integer) + -- doesn't go across keccak-buffer boundary + -- block_size_in_bytes is a multiple of 8, so, keccak-buffer contains integer number of qwords + if lanes_used >= total_lanes then + keccak_feed(lanes_lo, lanes_hi, "\0\0\0\0\0\0\0\0", 0, 8, 8) + lanes_used = 0 + end + qwords_qty = floor(math_min(qwords_qty, total_lanes - lanes_used)) + if hi_factor_keccak ~= 0 then + for j = 1, qwords_qty do + qwords[j] = HEX64(lanes_lo[lanes_used + j - 1 + lanes_index_base]) + end + else + for j = 1, qwords_qty do + qwords[j] = HEX(lanes_hi[lanes_used + j])..HEX(lanes_lo[lanes_used + j]) + end + end + lanes_used = lanes_used + qwords_qty + return + gsub(table_concat(qwords, "", 1, qwords_qty), "(..)(..)(..)(..)(..)(..)(..)(..)", "%8%7%6%5%4%3%2%1"), + qwords_qty * 8 + end + + local parts = {} -- digest parts + local last_part, last_part_size = "", 0 + + local function get_next_part_of_digest(bytes_needed) + -- returns 'bytes_needed' bytes, for arbitrary integer 'bytes_needed' + bytes_needed = bytes_needed or 1 + if bytes_needed <= last_part_size then + last_part_size = last_part_size - bytes_needed + local part_size_in_nibbles = bytes_needed * 2 + local result = sub(last_part, 1, part_size_in_nibbles) + last_part = sub(last_part, part_size_in_nibbles + 1) + return result + end + local parts_qty = 0 + if last_part_size > 0 then + parts_qty = 1 + parts[parts_qty] = last_part + bytes_needed = bytes_needed - last_part_size + end + -- repeats until the length is enough + while bytes_needed >= 8 do + local next_part, next_part_size = get_next_qwords_of_digest(bytes_needed / 8) + parts_qty = parts_qty + 1 + parts[parts_qty] = next_part + bytes_needed = bytes_needed - next_part_size + end + if bytes_needed > 0 then + last_part, last_part_size = get_next_qwords_of_digest(1) + parts_qty = parts_qty + 1 + parts[parts_qty] = get_next_part_of_digest(bytes_needed) + else + last_part, last_part_size = "", 0 + end + return table_concat(parts, "", 1, parts_qty) + end + + if digest_size_in_bytes < 0 then + result = get_next_part_of_digest + else + result = get_next_part_of_digest(digest_size_in_bytes) + end + end + return result + end + end + + if message then + -- Actually perform calculations and return the SHA-3 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get SHA-3 digest by invoking this function without an argument + return partial + end +end + + +local hex_to_bin, bin_to_hex, bin_to_base64, base64_to_bin +do + function hex_to_bin(hex_string) + return (gsub(hex_string, "%x%x", + function (hh) + return char(tonumber(hh, 16)) + end + )) + end + + function bin_to_hex(binary_string) + return (gsub(binary_string, ".", + function (c) + return string_format("%02x", byte(c)) + end + )) + end + + local base64_symbols = { + ['+'] = 62, ['-'] = 62, [62] = '+', + ['/'] = 63, ['_'] = 63, [63] = '/', + ['='] = -1, ['.'] = -1, [-1] = '=' + } + local symbol_index = 0 + for j, pair in ipairs{'AZ', 'az', '09'} do + for ascii = byte(pair), byte(pair, 2) do + local ch = char(ascii) + base64_symbols[ch] = symbol_index + base64_symbols[symbol_index] = ch + symbol_index = symbol_index + 1 + end + end + + function bin_to_base64(binary_string) + local result = {} + for pos = 1, #binary_string, 3 do + local c1, c2, c3, c4 = byte(sub(binary_string, pos, pos + 2)..'\0', 1, -1) + result[#result + 1] = + base64_symbols[floor(c1 / 4)] + ..base64_symbols[c1 % 4 * 16 + floor(c2 / 16)] + ..base64_symbols[c3 and c2 % 16 * 4 + floor(c3 / 64) or -1] + ..base64_symbols[c4 and c3 % 64 or -1] + end + return table_concat(result) + end + + function base64_to_bin(base64_string) + local result, chars_qty = {}, 3 + for pos, ch in gmatch(gsub(base64_string, '%s+', ''), '()(.)') do + local code = base64_symbols[ch] + if code < 0 then + chars_qty = chars_qty - 1 + code = 0 + end + local idx = pos % 4 + if idx > 0 then + result[-idx] = code + else + local c1 = result[-1] * 4 + floor(result[-2] / 16) + local c2 = (result[-2] % 16) * 16 + floor(result[-3] / 4) + local c3 = (result[-3] % 4) * 64 + code + result[#result + 1] = sub(char(c1, c2, c3), 1, chars_qty) + end + end + return table_concat(result) + end + +end + + +local block_size_for_HMAC -- this table will be initialized at the end of the module + +local function pad_and_xor(str, result_length, byte_for_xor) + return gsub(str, ".", + function(c) + return char(XOR_BYTE(byte(c), byte_for_xor)) + end + )..string_rep(char(byte_for_xor), result_length - #str) +end + +local function hmac(hash_func, key, message) + -- Create an instance (private objects for current calculation) + local block_size = block_size_for_HMAC[hash_func] + if not block_size then + error("Unknown hash function", 2) + end + if #key > block_size then + key = hex_to_bin(hash_func(key)) + end + local append = hash_func()(pad_and_xor(key, block_size, 0x36)) + local result + + local function partial(message_part) + if not message_part then + result = result or hash_func(pad_and_xor(key, block_size, 0x5C)..hex_to_bin(append())) + return result + elseif result then + error("Adding more chunks is not allowed after receiving the result", 2) + else + append(message_part) + return partial + end + end + + if message then + -- Actually perform calculations and return the HMAC of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading of a message + -- User should feed every chunk of the message as single argument to this function and finally get HMAC by invoking this function without an argument + return partial + end +end + + +local function xor_blake2_salt(salt, letter, H_lo, H_hi) + -- salt: concatenation of "Salt"+"Personalization" fields + local max_size = letter == "s" and 16 or 32 + local salt_size = #salt + if salt_size > max_size then + error(string_format("For BLAKE2%s/BLAKE2%sp/BLAKE2X%s the 'salt' parameter length must not exceed %d bytes", letter, letter, letter, max_size), 2) + end + if H_lo then + local offset, blake2_word_size, xor = 0, letter == "s" and 4 or 8, letter == "s" and XOR or XORA5 + for j = 5, 4 + ceil(salt_size / blake2_word_size) do + local prev, last + for _ = 1, blake2_word_size, 4 do + offset = offset + 4 + local a, b, c, d = byte(salt, offset - 3, offset) + local four_bytes = (((d or 0) * 256 + (c or 0)) * 256 + (b or 0)) * 256 + (a or 0) + prev, last = last, four_bytes + end + H_lo[j] = xor(H_lo[j], prev and last * hi_factor + prev or last) + if H_hi then + H_hi[j] = xor(H_hi[j], last) + end + end + end +end + +local function blake2s(message, key, salt, digest_size_in_bytes, XOF_length, B2_offset) + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 32 bytes, by default empty string + -- salt: (optional) binary string up to 16 bytes, by default empty string + -- digest_size_in_bytes: (optional) integer from 1 to 32, by default 32 + -- The last two parameters "XOF_length" and "B2_offset" are for internal use only, user must omit them (or pass nil) + digest_size_in_bytes = digest_size_in_bytes or 32 + if digest_size_in_bytes < 1 or digest_size_in_bytes > 32 then + error("BLAKE2s digest length must be from 1 to 32 bytes", 2) + end + key = key or "" + local key_length = #key + if key_length > 32 then + error("BLAKE2s key length must not exceed 32 bytes", 2) + end + salt = salt or "" + local bytes_compressed, tail, H = 0.0, "", {unpack(sha2_H_hi)} + if B2_offset then + H[1] = XOR(H[1], digest_size_in_bytes) + H[2] = XOR(H[2], 0x20) + H[3] = XOR(H[3], B2_offset) + H[4] = XOR(H[4], 0x20000000 + XOF_length) + else + H[1] = XOR(H[1], 0x01010000 + key_length * 256 + digest_size_in_bytes) + if XOF_length then + H[4] = XOR(H[4], XOF_length) + end + end + if salt ~= "" then + xor_blake2_salt(salt, "s", H) + end + + local function partial(message_part) + if message_part then + if tail then + local offs = 0 + if tail ~= "" and #tail + #message_part > 64 then + offs = 64 - #tail + bytes_compressed = blake2s_feed_64(H, tail..sub(message_part, 1, offs), 0, 64, bytes_compressed) + tail = "" + end + local size = #message_part - offs + local size_tail = size > 0 and (size - 1) % 64 + 1 or 0 + bytes_compressed = blake2s_feed_64(H, message_part, offs, size - size_tail, bytes_compressed) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + if B2_offset then + blake2s_feed_64(H, nil, 0, 64, 0, 32) + else + blake2s_feed_64(H, tail..string_rep("\0", 64 - #tail), 0, 64, bytes_compressed, #tail) + end + tail = nil + if not XOF_length or B2_offset then + local max_reg = ceil(digest_size_in_bytes / 4) + for j = 1, max_reg do + H[j] = HEX(H[j]) + end + H = sub(gsub(table_concat(H, "", 1, max_reg), "(..)(..)(..)(..)", "%4%3%2%1"), 1, digest_size_in_bytes * 2) + end + end + return H + end + end + + if key_length > 0 then + partial(key..string_rep("\0", 64 - key_length)) + end + if B2_offset then + return partial() + elseif message then + -- Actually perform calculations and return the BLAKE2s digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE2s digest by invoking this function without an argument + return partial + end +end + +local function blake2b(message, key, salt, digest_size_in_bytes, XOF_length, B2_offset) + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 64 bytes, by default empty string + -- salt: (optional) binary string up to 32 bytes, by default empty string + -- digest_size_in_bytes: (optional) integer from 1 to 64, by default 64 + -- The last two parameters "XOF_length" and "B2_offset" are for internal use only, user must omit them (or pass nil) + digest_size_in_bytes = floor(digest_size_in_bytes or 64) + if digest_size_in_bytes < 1 or digest_size_in_bytes > 64 then + error("BLAKE2b digest length must be from 1 to 64 bytes", 2) + end + key = key or "" + local key_length = #key + if key_length > 64 then + error("BLAKE2b key length must not exceed 64 bytes", 2) + end + salt = salt or "" + local bytes_compressed, tail, H_lo, H_hi = 0.0, "", {unpack(sha2_H_lo)}, not HEX64 and {unpack(sha2_H_hi)} + if B2_offset then + if H_hi then + H_lo[1] = XORA5(H_lo[1], digest_size_in_bytes) + H_hi[1] = XORA5(H_hi[1], 0x40) + H_lo[2] = XORA5(H_lo[2], B2_offset) + H_hi[2] = XORA5(H_hi[2], XOF_length) + else + H_lo[1] = XORA5(H_lo[1], 0x40 * hi_factor + digest_size_in_bytes) + H_lo[2] = XORA5(H_lo[2], XOF_length * hi_factor + B2_offset) + end + H_lo[3] = XORA5(H_lo[3], 0x4000) + else + H_lo[1] = XORA5(H_lo[1], 0x01010000 + key_length * 256 + digest_size_in_bytes) + if XOF_length then + if H_hi then + H_hi[2] = XORA5(H_hi[2], XOF_length) + else + H_lo[2] = XORA5(H_lo[2], XOF_length * hi_factor) + end + end + end + if salt ~= "" then + xor_blake2_salt(salt, "b", H_lo, H_hi) + end + + local function partial(message_part) + if message_part then + if tail then + local offs = 0 + if tail ~= "" and #tail + #message_part > 128 then + offs = 128 - #tail + bytes_compressed = blake2b_feed_128(H_lo, H_hi, tail..sub(message_part, 1, offs), 0, 128, bytes_compressed) + tail = "" + end + local size = #message_part - offs + local size_tail = size > 0 and (size - 1) % 128 + 1 or 0 + bytes_compressed = blake2b_feed_128(H_lo, H_hi, message_part, offs, size - size_tail, bytes_compressed) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + if B2_offset then + blake2b_feed_128(H_lo, H_hi, nil, 0, 128, 0, 64) + else + blake2b_feed_128(H_lo, H_hi, tail..string_rep("\0", 128 - #tail), 0, 128, bytes_compressed, #tail) + end + tail = nil + if XOF_length and not B2_offset then + if H_hi then + for j = 8, 1, -1 do + H_lo[j*2] = H_hi[j] + H_lo[j*2-1] = H_lo[j] + end + return H_lo, 16 + end + else + local max_reg = ceil(digest_size_in_bytes / 8) + if H_hi then + for j = 1, max_reg do + H_lo[j] = HEX(H_hi[j])..HEX(H_lo[j]) + end + else + for j = 1, max_reg do + H_lo[j] = HEX64(H_lo[j]) + end + end + H_lo = sub(gsub(table_concat(H_lo, "", 1, max_reg), "(..)(..)(..)(..)(..)(..)(..)(..)", "%8%7%6%5%4%3%2%1"), 1, digest_size_in_bytes * 2) + end + H_hi = nil + end + return H_lo + end + end + + if key_length > 0 then + partial(key..string_rep("\0", 128 - key_length)) + end + if B2_offset then + return partial() + elseif message then + -- Actually perform calculations and return the BLAKE2b digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE2b digest by invoking this function without an argument + return partial + end +end + +local function blake2sp(message, key, salt, digest_size_in_bytes) + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 32 bytes, by default empty string + -- salt: (optional) binary string up to 16 bytes, by default empty string + -- digest_size_in_bytes: (optional) integer from 1 to 32, by default 32 + digest_size_in_bytes = digest_size_in_bytes or 32 + if digest_size_in_bytes < 1 or digest_size_in_bytes > 32 then + error("BLAKE2sp digest length must be from 1 to 32 bytes", 2) + end + key = key or "" + local key_length = #key + if key_length > 32 then + error("BLAKE2sp key length must not exceed 32 bytes", 2) + end + salt = salt or "" + local instances, length, first_dword_of_parameter_block, result = {}, 0.0, 0x02080000 + key_length * 256 + digest_size_in_bytes + for j = 1, 8 do + local bytes_compressed, tail, H = 0.0, "", {unpack(sha2_H_hi)} + instances[j] = {bytes_compressed, tail, H} + H[1] = XOR(H[1], first_dword_of_parameter_block) + H[3] = XOR(H[3], j-1) + H[4] = XOR(H[4], 0x20000000) + if salt ~= "" then + xor_blake2_salt(salt, "s", H) + end + end + + local function partial(message_part) + if message_part then + if instances then + local from = 0 + while true do + local to = math_min(from + 64 - length % 64, #message_part) + if to > from then + local inst = instances[floor(length / 64) % 8 + 1] + local part = sub(message_part, from + 1, to) + length, from = length + to - from, to + local bytes_compressed, tail = inst[1], inst[2] + if #tail < 64 then + tail = tail..part + else + local H = inst[3] + bytes_compressed = blake2s_feed_64(H, tail, 0, 64, bytes_compressed) + tail = part + end + inst[1], inst[2] = bytes_compressed, tail + else + break + end + end + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if instances then + local root_H = {unpack(sha2_H_hi)} + root_H[1] = XOR(root_H[1], first_dword_of_parameter_block) + root_H[4] = XOR(root_H[4], 0x20010000) + if salt ~= "" then + xor_blake2_salt(salt, "s", root_H) + end + for j = 1, 8 do + local inst = instances[j] + local bytes_compressed, tail, H = inst[1], inst[2], inst[3] + blake2s_feed_64(H, tail..string_rep("\0", 64 - #tail), 0, 64, bytes_compressed, #tail, j == 8) + if j % 2 == 0 then + local index = 0 + for k = j - 1, j do + local inst = instances[k] + local H = inst[3] + for i = 1, 8 do + index = index + 1 + common_W_blake2s[index] = H[i] + end + end + blake2s_feed_64(root_H, nil, 0, 64, 64 * (j/2 - 1), j == 8 and 64, j == 8) + end + end + instances = nil + local max_reg = ceil(digest_size_in_bytes / 4) + for j = 1, max_reg do + root_H[j] = HEX(root_H[j]) + end + result = sub(gsub(table_concat(root_H, "", 1, max_reg), "(..)(..)(..)(..)", "%4%3%2%1"), 1, digest_size_in_bytes * 2) + end + return result + end + end + + if key_length > 0 then + key = key..string_rep("\0", 64 - key_length) + for j = 1, 8 do + partial(key) + end + end + if message then + -- Actually perform calculations and return the BLAKE2sp digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE2sp digest by invoking this function without an argument + return partial + end + +end + +local function blake2bp(message, key, salt, digest_size_in_bytes) + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 64 bytes, by default empty string + -- salt: (optional) binary string up to 32 bytes, by default empty string + -- digest_size_in_bytes: (optional) integer from 1 to 64, by default 64 + digest_size_in_bytes = digest_size_in_bytes or 64 + if digest_size_in_bytes < 1 or digest_size_in_bytes > 64 then + error("BLAKE2bp digest length must be from 1 to 64 bytes", 2) + end + key = key or "" + local key_length = #key + if key_length > 64 then + error("BLAKE2bp key length must not exceed 64 bytes", 2) + end + salt = salt or "" + local instances, length, first_dword_of_parameter_block, result = {}, 0.0, 0x02040000 + key_length * 256 + digest_size_in_bytes + for j = 1, 4 do + local bytes_compressed, tail, H_lo, H_hi = 0.0, "", {unpack(sha2_H_lo)}, not HEX64 and {unpack(sha2_H_hi)} + instances[j] = {bytes_compressed, tail, H_lo, H_hi} + H_lo[1] = XORA5(H_lo[1], first_dword_of_parameter_block) + H_lo[2] = XORA5(H_lo[2], j-1) + H_lo[3] = XORA5(H_lo[3], 0x4000) + if salt ~= "" then + xor_blake2_salt(salt, "b", H_lo, H_hi) + end + end + + local function partial(message_part) + if message_part then + if instances then + local from = 0 + while true do + local to = math_min(from + 128 - length % 128, #message_part) + if to > from then + local inst = instances[floor(length / 128) % 4 + 1] + local part = sub(message_part, from + 1, to) + length, from = length + to - from, to + local bytes_compressed, tail = inst[1], inst[2] + if #tail < 128 then + tail = tail..part + else + local H_lo, H_hi = inst[3], inst[4] + bytes_compressed = blake2b_feed_128(H_lo, H_hi, tail, 0, 128, bytes_compressed) + tail = part + end + inst[1], inst[2] = bytes_compressed, tail + else + break + end + end + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if instances then + local root_H_lo, root_H_hi = {unpack(sha2_H_lo)}, not HEX64 and {unpack(sha2_H_hi)} + root_H_lo[1] = XORA5(root_H_lo[1], first_dword_of_parameter_block) + root_H_lo[3] = XORA5(root_H_lo[3], 0x4001) + if salt ~= "" then + xor_blake2_salt(salt, "b", root_H_lo, root_H_hi) + end + for j = 1, 4 do + local inst = instances[j] + local bytes_compressed, tail, H_lo, H_hi = inst[1], inst[2], inst[3], inst[4] + blake2b_feed_128(H_lo, H_hi, tail..string_rep("\0", 128 - #tail), 0, 128, bytes_compressed, #tail, j == 4) + if j % 2 == 0 then + local index = 0 + for k = j - 1, j do + local inst = instances[k] + local H_lo, H_hi = inst[3], inst[4] + for i = 1, 8 do + index = index + 1 + common_W_blake2b[index] = H_lo[i] + if H_hi then + index = index + 1 + common_W_blake2b[index] = H_hi[i] + end + end + end + blake2b_feed_128(root_H_lo, root_H_hi, nil, 0, 128, 128 * (j/2 - 1), j == 4 and 128, j == 4) + end + end + instances = nil + local max_reg = ceil(digest_size_in_bytes / 8) + if HEX64 then + for j = 1, max_reg do + root_H_lo[j] = HEX64(root_H_lo[j]) + end + else + for j = 1, max_reg do + root_H_lo[j] = HEX(root_H_hi[j])..HEX(root_H_lo[j]) + end + end + result = sub(gsub(table_concat(root_H_lo, "", 1, max_reg), "(..)(..)(..)(..)(..)(..)(..)(..)", "%8%7%6%5%4%3%2%1"), 1, digest_size_in_bytes * 2) + end + return result + end + end + + if key_length > 0 then + key = key..string_rep("\0", 128 - key_length) + for j = 1, 4 do + partial(key) + end + end + if message then + -- Actually perform calculations and return the BLAKE2bp digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE2bp digest by invoking this function without an argument + return partial + end + +end + +local function blake2x(inner_func, inner_func_letter, common_W_blake2, block_size, digest_size_in_bytes, message, key, salt) + local XOF_digest_length_limit, XOF_digest_length, chunk_by_chunk_output = 2^(block_size / 2) - 1 + if digest_size_in_bytes == -1 then -- infinite digest + digest_size_in_bytes = math_huge + XOF_digest_length = floor(XOF_digest_length_limit) + chunk_by_chunk_output = true + else + if digest_size_in_bytes < 0 then + digest_size_in_bytes = -1.0 * digest_size_in_bytes + chunk_by_chunk_output = true + end + XOF_digest_length = floor(digest_size_in_bytes) + if XOF_digest_length >= XOF_digest_length_limit then + error("Requested digest is too long. BLAKE2X"..inner_func_letter.." finite digest is limited by (2^"..floor(block_size / 2)..")-2 bytes. Hint: you can generate infinite digest.", 2) + end + end + salt = salt or "" + if salt ~= "" then + xor_blake2_salt(salt, inner_func_letter) -- don't xor, only check the size of salt + end + local inner_partial = inner_func(nil, key, salt, nil, XOF_digest_length) + local result + + local function partial(message_part) + if message_part then + if inner_partial then + inner_partial(message_part) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if inner_partial then + local half_W, half_W_size = inner_partial() + half_W_size, inner_partial = half_W_size or 8 + + local function get_hash_block(block_no) + -- block_no = 0...(2^32-1) + local size = math_min(block_size, digest_size_in_bytes - block_no * block_size) + if size <= 0 then + return "" + end + for j = 1, half_W_size do + common_W_blake2[j] = half_W[j] + end + for j = half_W_size + 1, 2 * half_W_size do + common_W_blake2[j] = 0 + end + return inner_func(nil, nil, salt, size, XOF_digest_length, floor(block_no)) + end + + local hash = {} + if chunk_by_chunk_output then + local pos, period, cached_block_no, cached_block = 0, block_size * 2^32 + + local function get_next_part_of_digest(arg1, arg2) + if arg1 == "seek" then + -- Usage #1: get_next_part_of_digest("seek", new_pos) + pos = arg2 % period + else + -- Usage #2: hex_string = get_next_part_of_digest(size) + local size, index = arg1 or 1, 0 + while size > 0 do + local block_offset = pos % block_size + local block_no = (pos - block_offset) / block_size + local part_size = math_min(size, block_size - block_offset) + if cached_block_no ~= block_no then + cached_block_no = block_no + cached_block = get_hash_block(block_no) + end + index = index + 1 + hash[index] = sub(cached_block, block_offset * 2 + 1, (block_offset + part_size) * 2) + size = size - part_size + pos = (pos + part_size) % period + end + return table_concat(hash, "", 1, index) + end + end + + result = get_next_part_of_digest + else + for j = 1.0, ceil(digest_size_in_bytes / block_size) do + hash[j] = get_hash_block(j - 1.0) + end + result = table_concat(hash) + end + end + return result + end + end + + if message then + -- Actually perform calculations and return the BLAKE2X digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE2X digest by invoking this function without an argument + return partial + end +end + +local function blake2xs(digest_size_in_bytes, message, key, salt) + -- digest_size_in_bytes: + -- 0..65534 = get finite digest as single Lua string + -- (-1) = get infinite digest in "chunk-by-chunk" output mode + -- (-2)..(-65534) = get finite digest in "chunk-by-chunk" output mode + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 32 bytes, by default empty string + -- salt: (optional) binary string up to 16 bytes, by default empty string + return blake2x(blake2s, "s", common_W_blake2s, 32, digest_size_in_bytes, message, key, salt) +end + +local function blake2xb(digest_size_in_bytes, message, key, salt) + -- digest_size_in_bytes: + -- 0..4294967294 = get finite digest as single Lua string + -- (-1) = get infinite digest in "chunk-by-chunk" output mode + -- (-2)..(-4294967294) = get finite digest in "chunk-by-chunk" output mode + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 64 bytes, by default empty string + -- salt: (optional) binary string up to 32 bytes, by default empty string + return blake2x(blake2b, "b", common_W_blake2b, 64, digest_size_in_bytes, message, key, salt) +end + + +local function blake3(message, key, digest_size_in_bytes, message_flags, K, return_array) + -- message: binary string to be hashed (or nil for "chunk-by-chunk" input mode) + -- key: (optional) binary string up to 32 bytes, by default empty string + -- digest_size_in_bytes: (optional) by default 32 + -- 0,1,2,3,4,... = get finite digest as single Lua string + -- (-1) = get infinite digest in "chunk-by-chunk" output mode + -- -2,-3,-4,... = get finite digest in "chunk-by-chunk" output mode + -- The last three parameters "message_flags", "K" and "return_array" are for internal use only, user must omit them (or pass nil) + key = key or "" + digest_size_in_bytes = digest_size_in_bytes or 32 + message_flags = message_flags or 0 + if key == "" then + K = K or sha2_H_hi + else + local key_length = #key + if key_length > 32 then + error("BLAKE3 key length must not exceed 32 bytes", 2) + end + key = key..string_rep("\0", 32 - key_length) + K = {} + for j = 1, 8 do + local a, b, c, d = byte(key, 4*j-3, 4*j) + K[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + message_flags = message_flags + 16 -- flag:KEYED_HASH + end + local tail, H, chunk_index, blocks_in_chunk, stack_size, stack = "", {}, 0, 0, 0, {} + local final_H_in, final_block_length, chunk_by_chunk_output, result, wide_output = K + local final_compression_flags = 3 -- flags:CHUNK_START,CHUNK_END + + local function feed_blocks(str, offs, size) + -- size >= 0, size is multiple of 64 + while size > 0 do + local part_size_in_blocks, block_flags, H_in = 1, 0, H + if blocks_in_chunk == 0 then + block_flags = 1 -- flag:CHUNK_START + H_in, final_H_in = K, H + final_compression_flags = 2 -- flag:CHUNK_END + elseif blocks_in_chunk == 15 then + block_flags = 2 -- flag:CHUNK_END + final_compression_flags = 3 -- flags:CHUNK_START,CHUNK_END + final_H_in = K + else + part_size_in_blocks = math_min(size / 64, 15 - blocks_in_chunk) + end + local part_size = part_size_in_blocks * 64 + blake3_feed_64(str, offs, part_size, message_flags + block_flags, chunk_index, H_in, H) + offs, size = offs + part_size, size - part_size + blocks_in_chunk = (blocks_in_chunk + part_size_in_blocks) % 16 + if blocks_in_chunk == 0 then + -- completing the currect chunk + chunk_index = chunk_index + 1.0 + local divider = 2.0 + while chunk_index % divider == 0 do + divider = divider * 2.0 + stack_size = stack_size - 8 + for j = 1, 8 do + common_W_blake2s[j] = stack[stack_size + j] + end + for j = 1, 8 do + common_W_blake2s[j + 8] = H[j] + end + blake3_feed_64(nil, 0, 64, message_flags + 4, 0, K, H) -- flag:PARENT + end + for j = 1, 8 do + stack[stack_size + j] = H[j] + end + stack_size = stack_size + 8 + end + end + end + + local function get_hash_block(block_no) + local size = math_min(64, digest_size_in_bytes - block_no * 64) + if block_no < 0 or size <= 0 then + return "" + end + if chunk_by_chunk_output then + for j = 1, 16 do + common_W_blake2s[j] = stack[j + 16] + end + end + blake3_feed_64(nil, 0, 64, final_compression_flags, block_no, final_H_in, stack, wide_output, final_block_length) + if return_array then + return stack + end + local max_reg = ceil(size / 4) + for j = 1, max_reg do + stack[j] = HEX(stack[j]) + end + return sub(gsub(table_concat(stack, "", 1, max_reg), "(..)(..)(..)(..)", "%4%3%2%1"), 1, size * 2) + end + + local function partial(message_part) + if message_part then + if tail then + local offs = 0 + if tail ~= "" and #tail + #message_part > 64 then + offs = 64 - #tail + feed_blocks(tail..sub(message_part, 1, offs), 0, 64) + tail = "" + end + local size = #message_part - offs + local size_tail = size > 0 and (size - 1) % 64 + 1 or 0 + feed_blocks(message_part, offs, size - size_tail) + tail = tail..sub(message_part, #message_part + 1 - size_tail) + return partial + else + error("Adding more chunks is not allowed after receiving the result", 2) + end + else + if tail then + final_block_length = #tail + tail = tail..string_rep("\0", 64 - #tail) + if common_W_blake2s[0] then + for j = 1, 16 do + local a, b, c, d = byte(tail, 4*j-3, 4*j) + common_W_blake2s[j] = OR(SHL(d, 24), SHL(c, 16), SHL(b, 8), a) + end + else + for j = 1, 16 do + local a, b, c, d = byte(tail, 4*j-3, 4*j) + common_W_blake2s[j] = ((d * 256 + c) * 256 + b) * 256 + a + end + end + tail = nil + for stack_size = stack_size - 8, 0, -8 do + blake3_feed_64(nil, 0, 64, message_flags + final_compression_flags, chunk_index, final_H_in, H, nil, final_block_length) + chunk_index, final_block_length, final_H_in, final_compression_flags = 0, 64, K, 4 -- flag:PARENT + for j = 1, 8 do + common_W_blake2s[j] = stack[stack_size + j] + end + for j = 1, 8 do + common_W_blake2s[j + 8] = H[j] + end + end + final_compression_flags = message_flags + final_compression_flags + 8 -- flag:ROOT + if digest_size_in_bytes < 0 then + if digest_size_in_bytes == -1 then -- infinite digest + digest_size_in_bytes = math_huge + else + digest_size_in_bytes = -1.0 * digest_size_in_bytes + end + chunk_by_chunk_output = true + for j = 1, 16 do + stack[j + 16] = common_W_blake2s[j] + end + end + digest_size_in_bytes = math_min(2^53, digest_size_in_bytes) + wide_output = digest_size_in_bytes > 32 + if chunk_by_chunk_output then + local pos, cached_block_no, cached_block = 0.0 + + local function get_next_part_of_digest(arg1, arg2) + if arg1 == "seek" then + -- Usage #1: get_next_part_of_digest("seek", new_pos) + pos = arg2 * 1.0 + else + -- Usage #2: hex_string = get_next_part_of_digest(size) + local size, index = arg1 or 1, 32 + while size > 0 do + local block_offset = pos % 64 + local block_no = (pos - block_offset) / 64 + local part_size = math_min(size, 64 - block_offset) + if cached_block_no ~= block_no then + cached_block_no = block_no + cached_block = get_hash_block(block_no) + end + index = index + 1 + stack[index] = sub(cached_block, block_offset * 2 + 1, (block_offset + part_size) * 2) + size = size - part_size + pos = pos + part_size + end + return table_concat(stack, "", 33, index) + end + end + + result = get_next_part_of_digest + elseif digest_size_in_bytes <= 64 then + result = get_hash_block(0) + else + local last_block_no = ceil(digest_size_in_bytes / 64) - 1 + for block_no = 0.0, last_block_no do + stack[33 + block_no] = get_hash_block(block_no) + end + result = table_concat(stack, "", 33, 33 + last_block_no) + end + end + return result + end + end + + if message then + -- Actually perform calculations and return the BLAKE3 digest of a message + return partial(message)() + else + -- Return function for chunk-by-chunk loading + -- User should feed every chunk of input data as single argument to this function and finally get BLAKE3 digest by invoking this function without an argument + return partial + end +end + +local function blake3_derive_key(key_material, context_string, derived_key_size_in_bytes) + -- key_material: (string) your source of entropy to derive a key from (for example, it can be a master password) + -- set to nil for feeding the key material in "chunk-by-chunk" input mode + -- context_string: (string) unique description of the derived key + -- digest_size_in_bytes: (optional) by default 32 + -- 0,1,2,3,4,... = get finite derived key as single Lua string + -- (-1) = get infinite derived key in "chunk-by-chunk" output mode + -- -2,-3,-4,... = get finite derived key in "chunk-by-chunk" output mode + if type(context_string) ~= "string" then + error("'context_string' parameter must be a Lua string", 2) + end + local K = blake3(context_string, nil, nil, 32, nil, true) -- flag:DERIVE_KEY_CONTEXT + return blake3(key_material, nil, derived_key_size_in_bytes, 64, K) -- flag:DERIVE_KEY_MATERIAL +end + + + +local sha = { + md5 = md5, -- MD5 + sha1 = sha1, -- SHA-1 + -- SHA-2 hash functions: + sha224 = function (message) return sha256ext(224, message) end, -- SHA-224 + sha256 = function (message) return sha256ext(256, message) end, -- SHA-256 + sha512_224 = function (message) return sha512ext(224, message) end, -- SHA-512/224 + sha512_256 = function (message) return sha512ext(256, message) end, -- SHA-512/256 + sha384 = function (message) return sha512ext(384, message) end, -- SHA-384 + sha512 = function (message) return sha512ext(512, message) end, -- SHA-512 + -- SHA-3 hash functions: + sha3_224 = function (message) return keccak((1600 - 2 * 224) / 8, 224 / 8, false, message) end, -- SHA3-224 + sha3_256 = function (message) return keccak((1600 - 2 * 256) / 8, 256 / 8, false, message) end, -- SHA3-256 + sha3_384 = function (message) return keccak((1600 - 2 * 384) / 8, 384 / 8, false, message) end, -- SHA3-384 + sha3_512 = function (message) return keccak((1600 - 2 * 512) / 8, 512 / 8, false, message) end, -- SHA3-512 + shake128 = function (digest_size_in_bytes, message) return keccak((1600 - 2 * 128) / 8, digest_size_in_bytes, true, message) end, -- SHAKE128 + shake256 = function (digest_size_in_bytes, message) return keccak((1600 - 2 * 256) / 8, digest_size_in_bytes, true, message) end, -- SHAKE256 + -- HMAC: + hmac = hmac, -- HMAC(hash_func, key, message) is applicable to any hash function from this module except SHAKE* and BLAKE* + -- misc utilities: + hex_to_bin = hex_to_bin, -- converts hexadecimal representation to binary string + bin_to_hex = bin_to_hex, -- converts binary string to hexadecimal representation + base64_to_bin = base64_to_bin, -- converts base64 representation to binary string + bin_to_base64 = bin_to_base64, -- converts binary string to base64 representation + -- old style names for backward compatibility: + hex2bin = hex_to_bin, + bin2hex = bin_to_hex, + base642bin = base64_to_bin, + bin2base64 = bin_to_base64, + -- BLAKE2 hash functions: + blake2b = blake2b, -- BLAKE2b (message, key, salt, digest_size_in_bytes) + blake2s = blake2s, -- BLAKE2s (message, key, salt, digest_size_in_bytes) + blake2bp = blake2bp, -- BLAKE2bp(message, key, salt, digest_size_in_bytes) + blake2sp = blake2sp, -- BLAKE2sp(message, key, salt, digest_size_in_bytes) + blake2xb = blake2xb, -- BLAKE2Xb(digest_size_in_bytes, message, key, salt) + blake2xs = blake2xs, -- BLAKE2Xs(digest_size_in_bytes, message, key, salt) + -- BLAKE2 aliases: + blake2 = blake2b, + blake2b_160 = function (message, key, salt) return blake2b(message, key, salt, 20) end, -- BLAKE2b-160 + blake2b_256 = function (message, key, salt) return blake2b(message, key, salt, 32) end, -- BLAKE2b-256 + blake2b_384 = function (message, key, salt) return blake2b(message, key, salt, 48) end, -- BLAKE2b-384 + blake2b_512 = blake2b, -- 64 -- BLAKE2b-512 + blake2s_128 = function (message, key, salt) return blake2s(message, key, salt, 16) end, -- BLAKE2s-128 + blake2s_160 = function (message, key, salt) return blake2s(message, key, salt, 20) end, -- BLAKE2s-160 + blake2s_224 = function (message, key, salt) return blake2s(message, key, salt, 28) end, -- BLAKE2s-224 + blake2s_256 = blake2s, -- 32 -- BLAKE2s-256 + -- BLAKE3 hash function + blake3 = blake3, -- BLAKE3 (message, key, digest_size_in_bytes) + blake3_derive_key = blake3_derive_key, -- BLAKE3_KDF(key_material, context_string, derived_key_size_in_bytes) +} + + +block_size_for_HMAC = { + [sha.md5] = 64, + [sha.sha1] = 64, + [sha.sha224] = 64, + [sha.sha256] = 64, + [sha.sha512_224] = 128, + [sha.sha512_256] = 128, + [sha.sha384] = 128, + [sha.sha512] = 128, + [sha.sha3_224] = 144, -- (1600 - 2 * 224) / 8 + [sha.sha3_256] = 136, -- (1600 - 2 * 256) / 8 + [sha.sha3_384] = 104, -- (1600 - 2 * 384) / 8 + [sha.sha3_512] = 72, -- (1600 - 2 * 512) / 8 +} + + +return sha diff --git a/runtime/lua/socket.lua b/runtime/lua/socket.lua new file mode 100644 index 0000000000..dec749aca5 --- /dev/null +++ b/runtime/lua/socket.lua @@ -0,0 +1,148 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") + +local _M = socket + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function _M.connect4(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet") +end + +function _M.connect6(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet6") +end + +function _M.bind(host, port, backlog) + if host == "*" then host = "0.0.0.0" end + local addrinfo, err = socket.dns.getaddrinfo(host); + if not addrinfo then return nil, err end + local sock, res + err = "no info on address" + for i, alt in base.ipairs(addrinfo) do + if alt.family == "inet" then + sock, err = socket.tcp4() + else + sock, err = socket.tcp6() + end + if not sock then return nil, err end + res, err = sock:bind(alt.addr, port) + if not res then + sock:close() + else + res, err = sock:listen(backlog) + if not res then + sock:close() + else + return sock + end + end + end + return nil, err +end + +_M.try = _M.newtry() + +function _M.choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +local sourcet, sinkt = {}, {} +_M.sourcet = sourcet +_M.sinkt = sinkt + +_M.BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +_M.sink = _M.choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +_M.source = _M.choose(sourcet) + +return _M diff --git a/spec/System/SampleCharacter.json b/spec/System/SampleCharacter.json new file mode 100644 index 0000000000..0e7917d66b --- /dev/null +++ b/spec/System/SampleCharacter.json @@ -0,0 +1,3886 @@ +{ + "character": { + "id": "notrelevant", + "name": "testcharacter", + "realm": "pc", + "class": "Hierophant", + "league": "Mirage", + "level": 99, + "experience": 4017660065, + "equipment": [ + { + "verified": false, + "w": 1, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzksMTQseyJmIjoiMkRJdGVtcy9GbGFza3MvZGlhbW9uZCIsInciOjEsImgiOjIsInNjYWxlIjoxLCJsZXZlbCI6MSwiZmkiOnRydWV9XQ/a764b9bc68/diamond.png", + "league": "Mirage", + "id": "948f29ccaab4558a800a8f554134531acbba45f8c4149b34fc029d2a73bd8d98", + "name": "", + "typeLine": "Doctor's Diamond Flask of the Cheetah", + "baseType": "Diamond Flask", + "rarity": "Magic", + "ilvl": 85, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Lasts {0} Seconds", + "values": [["7.20", 1]], + "displayMode": 3 + }, + { + "name": "Consumes {0} of {1} Charges on use", + "values": [["20", 0], ["40", 0]], + "displayMode": 3 + }, + { + "name": "Currently has {0} Charges", + "values": [["40", 0]], + "displayMode": 3 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["68", 0]], + "displayMode": 0, + "type": 62 + } + ], + "utilityMods": ["100% increased Global Critical Strike Chance"], + "enchantMods": ["Used when Charges reach full"], + "explicitMods": [ + "22% chance to gain a Flask Charge when you deal a Critical Strike", + "13% increased Movement Speed during Effect" + ], + "descrText": "Right click to drink. Can only hold charges while in belt. Refills as you kill monsters.", + "frameType": 1, + "x": 3, + "y": 0, + "inventoryId": "Flask" + }, + { + "verified": false, + "w": 1, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzksMTQseyJmIjoiMkRJdGVtcy9GbGFza3Mvc3ByaW50IiwidyI6MSwiaCI6Miwic2NhbGUiOjEsImxldmVsIjoxLCJmaSI6dHJ1ZX1d/3f8edd74f2/sprint.png", + "league": "Mirage", + "id": "60e6a3e3bbb953e67616954941b5bf6942aa11982bc2c4556fad50ca3a1c6d95", + "name": "", + "typeLine": "Doctor's Quicksilver Flask of Incision", + "baseType": "Quicksilver Flask", + "rarity": "Magic", + "ilvl": 83, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Lasts {0} Seconds", + "values": [["7.20", 1]], + "displayMode": 3 + }, + { + "name": "Consumes {0} of {1} Charges on use", + "values": [["30", 0], ["60", 0]], + "displayMode": 3 + }, + { + "name": "Currently has {0} Charges", + "values": [["60", 0]], + "displayMode": 3 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["65", 0]], + "displayMode": 0, + "type": 62 + } + ], + "utilityMods": ["40% increased Movement Speed"], + "enchantMods": ["Used when Charges reach full"], + "explicitMods": [ + "21% chance to gain a Flask Charge when you deal a Critical Strike", + "51% increased Critical Strike Chance during Effect" + ], + "descrText": "Right click to drink. Can only hold charges while in belt. Refills as you kill monsters.", + "frameType": 1, + "x": 4, + "y": 0, + "inventoryId": "Flask" + }, + { + "verified": false, + "w": 1, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzksMTQseyJmIjoiMkRJdGVtcy9GbGFza3MvVmlhbG9mVW5saWZlIiwidyI6MSwiaCI6Miwic2NhbGUiOjEsImxldmVsIjoxLCJmaSI6dHJ1ZX1d/64bbacc4d4/VialofUnlife.png", + "league": "Mirage", + "id": "e5082aa7c21700e77402f05862b0416efde2527615aeb2928117ae1de74ded3f", + "name": "Cinderswallow Urn", + "typeLine": "Silver Flask", + "baseType": "Silver Flask", + "rarity": "Unique", + "ilvl": 85, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Lasts {0} Seconds", + "values": [["7.20", 1]], + "displayMode": 3 + }, + { + "name": "Consumes {0} of {1} Charges on use", + "values": [["40", 0], ["77", 1]], + "displayMode": 3 + }, + { + "name": "Currently has {0} Charges", + "values": [["77", 0]], + "displayMode": 3 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["48", 0]], + "displayMode": 0, + "type": 62 + } + ], + "utilityMods": ["Onslaught"], + "enchantMods": ["Used when Charges reach full"], + "explicitMods": [ + "+17 to Maximum Charges", + "Recharges 5 Charges when you Consume an Ignited corpse", + "Leech 1.5% of Expected Ignite Damage as Life when you Ignite an Enemy during Effect", + "Enemies Ignited by you during Effect take 8% increased Damage", + "Recover 3% of Mana when you Kill an Enemy during Effect" + ], + "descrText": "Right click to drink. Can only hold charges while in belt. Refills as you kill monsters.", + "flavourText": [ + "A controlled burn is sometimes necessary for new life." + ], + "frameType": 3, + "x": 2, + "y": 0, + "inventoryId": "Flask" + }, + { + "verified": false, + "w": 1, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzksMTQseyJmIjoiMkRJdGVtcy9GbGFza3MvQmxvY2tGbGFzayIsInciOjEsImgiOjIsInNjYWxlIjoxLCJsZXZlbCI6MSwiZmkiOnRydWV9XQ/4d59967889/BlockFlask.png", + "league": "Mirage", + "id": "11762744912542fb3544a3ac66b9057edfe7d19bf97a0bbaad00970f17fc3514", + "name": "Rumi's Concoction", + "typeLine": "Granite Flask", + "baseType": "Granite Flask", + "rarity": "Unique", + "ilvl": 85, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Lasts {0} Seconds", + "values": [["7.20", 1]], + "displayMode": 3 + }, + { + "name": "Consumes {0} of {1} Charges on use", + "values": [["30", 0], ["60", 0]], + "displayMode": 3 + }, + { + "name": "Currently has {0} Charges", + "values": [["60", 0]], + "displayMode": 3 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["68", 0]], + "displayMode": 0, + "type": 62 + } + ], + "utilityMods": ["+1500 to Armour"], + "enchantMods": ["Used when Charges reach full"], + "explicitMods": [ + "+11% Chance to Block Attack Damage during Effect", + "+4% Chance to Block Spell Damage during Effect" + ], + "descrText": "Right click to drink. Can only hold charges while in belt. Refills as you kill monsters.", + "flavourText": [ + "\"Yesterday I was clever, so I wanted to change the world.\r", + "Today I am wise, so I am changing myself.\"\r", + "-Rumi of the Vaal" + ], + "frameType": 3, + "x": 1, + "y": 0, + "inventoryId": "Flask" + }, + { + "verified": false, + "w": 2, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQmVsdHMvSGVhZGh1bnRlciIsInciOjIsImgiOjEsInNjYWxlIjoxfV0/e9542ec6ee/Headhunter.png", + "league": "Mirage", + "id": "45ce33d3c7e152aa33ec312a39a97f67a507d30f08436f25d699a76834408e98", + "name": "Headhunter", + "typeLine": "Leather Belt", + "baseType": "Leather Belt", + "rarity": "Unique", + "ilvl": 85, + "identified": true, + "properties": [ + { + "name": "Quality (Attribute Modifiers)", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["40", 0]], + "displayMode": 0, + "type": 62 + } + ], + "implicitMods": ["+40 to maximum Life"], + "explicitMods": [ + "+54 to Strength", + "+62 to Dexterity", + "+51 to maximum Life", + "22% increased Damage with Hits against Rare monsters", + "When you Kill a Rare monster, you gain its Modifiers for 60 seconds" + ], + "flavourText": [ + "\"A man's soul rules from a cavern of bone, learns and\r", + "judges through flesh-born windows. The heart is meat.\r", + "The head is where the Man is.\"\r", + "- Lavianga, Advisor to Kaom" + ], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "Belt" + }, + { + "verified": false, + "w": 1, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzksMTQseyJmIjoiMkRJdGVtcy9GbGFza3MvQm90dGxlZEZ1dHVyZXMiLCJ3IjoxLCJoIjoyLCJzY2FsZSI6MSwibGV2ZWwiOjEsImZpIjp0cnVlfV0/b19cc88881/BottledFutures.png", + "league": "Mirage", + "id": "f58b33cedc42948811148927623b5536efb46f03f60401cee38c7bb9f6458089", + "name": "Wine of the Prophet", + "typeLine": "Gold Flask", + "baseType": "Gold Flask", + "rarity": "Unique", + "ilvl": 86, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Lasts {0} Seconds", + "values": [["6", 1]], + "displayMode": 3 + }, + { + "name": "Consumes {0} of {1} Charges on use", + "values": [["86", 1], ["140", 1]], + "displayMode": 3 + }, + { + "name": "Currently has {0} Charges", + "values": [["140", 0]], + "displayMode": 3 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["33", 0]], + "displayMode": 0, + "type": 62 + } + ], + "utilityMods": ["30% increased Rarity of Items found"], + "enchantMods": ["Used when Charges reach full"], + "explicitMods": [ + "+60 to Maximum Charges", + "44% increased Charges per use", + "Grants a random Divination Buff for 20 seconds when Used" + ], + "descrText": "Right click to drink. Can only hold charges while in belt. Refills as you kill monsters.", + "flavourText": [ + "\"To proclaim a vision of the future is to make it so,\r", + "for those that have the will, and the way!\"\r", + "- High Templar Andronicus" + ], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "Flask" + }, + { + "verified": false, + "w": 2, + "h": 3, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQXJtb3Vycy9Cb2R5QXJtb3Vycy9Cb2R5U3RyRGV4SW50MUMiLCJ3IjoyLCJoIjozLCJzY2FsZSI6MSwic2VhcmluZyI6dHJ1ZSwidGFuZ2xlZCI6dHJ1ZSwiZnJhY3R1cmVkIjp0cnVlfV0/090e967242/BodyStrDexInt1C.png", + "league": "Mirage", + "id": "06ec0016b07f497cc836da843621332014a8b4f43021f378b15628efd588ce06", + "searing": true, + "tangled": true, + "fractured": true, + "sockets": [ + { + "group": 0, + "attr": "D", + "sColour": "G" + }, + { + "group": 0, + "attr": "D", + "sColour": "G" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + } + ], + "name": "Chimeric Guardian", + "typeLine": "Twilight Regalia", + "baseType": "Twilight Regalia", + "rarity": "Rare", + "ilvl": 86, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Energy Shield", + "values": [["950", 1]], + "displayMode": 0, + "type": 18 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["84", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["120", 0]], + "displayMode": 1, + "type": 64, + "suffix": "(gem)" + }, + { + "name": "Int", + "values": [["293", 0]], + "displayMode": 1, + "type": 65 + } + ], + "implicitMods": [ + "50% of Damage from your Hits cannot be Reflected", + "+22% to Critical Strike Multiplier for Attack Damage" + ], + "explicitMods": [ + "+52 to Intelligence", + "105% increased Energy Shield", + "+75 to maximum Mana", + "+38% to Lightning Resistance" + ], + "craftedMods": ["6% increased Attributes"], + "fracturedMods": ["+93 to maximum Energy Shield"], + "frameType": 2, + "x": 0, + "y": 0, + "inventoryId": "BodyArmour", + "socketedItems": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0ZvcmsiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwiZ2UiOnRydWV9XQ/27f7503962/Fork.png", + "support": true, + "league": "Mirage", + "id": "9d2bc560653c7aebf91bc3fdea8d10ccf8fc09b4d11640e41d074628214b674a", + "name": "", + "typeLine": "Greater Fork Support", + "baseType": "Greater Fork Support", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Exceptional, Support, Projectile", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["3 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["140%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+23%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["76", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["120", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports projectile skills, making their projectiles fork into two projectiles the first two times they hit an enemy and don't pierce it.", + "explicitMods": [ + "Projectiles from Supported Skills Fork", + "Projectiles from Supported Skills can Fork an additional Time", + "Projectiles from Supported Skills have 50% chance for an additional\nProjectile when Forking", + "Supported Skills deal 11% increased Projectile Damage" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "flavourText": [ + "Lioneye earned his moniker by killing\r", + "three men with a single arrow." + ], + "frameType": 4, + "socket": 0, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L1JldHVyblByb2plY3RpbGVzIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/76e59b7e35/ReturnProjectiles.png", + "support": true, + "league": "Mirage", + "id": "2e560483f065507189bfbcac7bff5b809285fe96d2ce5c11c3c34dc7fa7b19a2", + "name": "", + "typeLine": "Returning Projectiles Support", + "baseType": "Returning Projectiles Support", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Support, Projectile", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["150%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["111", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports projectile skills.", + "explicitMods": [ + "Projectiles from Supported Skills Return to you", + "Projectiles from Supported Skills deal 66% less Damage with Hits and Ailments while Returning", + "Projectiles from Supported Skills Pierce all Targets while Returning", + "Supported Skills have 10% increased Projectile Speed" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 1, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0luY3JlYXNlZENyaXRpY2FsRGFtYWdlIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/74164474d1/IncreasedCriticalDamage.png", + "support": true, + "league": "Mirage", + "id": "d76fdb80bc237aa3354a2e51c06140f2bb923b1918a61b6360d757a2b39fc267", + "name": "", + "typeLine": "Increased Critical Damage Support", + "baseType": "Increased Critical Damage Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Critical, Support", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["130%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["111", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Supports any skill that hits enemies.", + "explicitMods": [ + "Supported Skills have +148% to Critical Strike Multiplier" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 2, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0ludmVydHRoZVJ1bGVzIiwidyI6MSwiaCI6MSwic2NhbGUiOjEsImdlIjp0cnVlfV0/0f08d4a859/InverttheRules.png", + "support": true, + "league": "Mirage", + "id": "8735c142068a1e3c7614e905a77c21962acf4fe61ac28a59bdf291869eaa7974", + "name": "", + "typeLine": "Invert the Rules Support", + "baseType": "Invert the Rules Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Exceptional, Support", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["3 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["140%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["76", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["120", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Supports any skill that hits enemies.", + "explicitMods": [ + "Supported Skills deal 10% increased Elemental Damage", + "Hits from Supported Skills have 41% chance to treat Enemy Monster Elemental Resistance values as inverted" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "flavourText": [ + "\"She fled from the prison of her birth, drawn by the breaking\r", + "of that great Silence. At that moment, the universe was changed,\r", + "and walls that were once impenetrable became brittle.\r", + "A fortress became a shell, and she alone escaped.\"" + ], + "frameType": 4, + "socket": 3, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9DbHVzdGVyQnVyc3QiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwiZ2QiOjEzfV0/4fcc032863/ClusterBurst.png", + "support": false, + "league": "Mirage", + "id": "380fcd8bfa4000bfcf66f37477ba6ca3dee894122e9276d55eb74ad55ce1f8e1", + "name": "", + "typeLine": "Kinetic Blast of Clustering", + "baseType": "Kinetic Blast of Clustering", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Attack, Projectile, AoE, Physical", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["9 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Attack Speed", + "values": [["115% of base", 0]], + "displayMode": 0 + }, + { + "name": "Attack Damage", + "values": [["100% of base", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["155", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Fires a projectile from a Wand that causes a series of area explosions in a secondary radius around its point of impact, each damaging enemies.", + "explicitMods": [ + "Kinetic Blast creates 4 additional explosions", + "Increases and Reductions to Spell Damage also apply to Attack Damage from this Skill at 200% of their value", + "Deals Added Physical Damage equal to 17% of Maximum Mana", + "Modifiers to number of Projectiles instead apply to the number of Explosions", + "Base explosion radius is 1.9 metres", + "Base secondary radius is 2.8 metres", + "Projectiles cannot Split" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 4, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L1dhbmRXaXNwU3VwcG9ydCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/20976859e1/WandWispSupport.png", + "support": true, + "league": "Mirage", + "id": "9f6dc0daee15f196524925bb0044deb5fee7816b9afaff9359f008649fed2585", + "name": "", + "typeLine": "Summon Sacred Wisps", + "baseType": "Sacred Wisps Support", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Attack, Support, Trigger, Duration, Spell", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["140%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["111", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Supports attack skills that can be used with Wands. Cannot support Vaal skills, minion skills, movement skills, or skills used by totems, traps, or mines.", + "explicitMods": [ + "Supported Skills deal 10% increased Attack Damage", + "Supported Skills deal 51% less Damage when used by Sacred Wisps", + "Supported Skills will Trigger Summon Sacred Wisps on Hit", + "Supported Skills can only be used with Wands" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 5, + "colour": "I", + "hybrid": { + "baseTypeName": "Sacred Wisps Support", + "properties": [ + { + "name": "Cast Time", + "values": [["1.00 sec", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "Base duration is 10.00 seconds", + "Summons 2 Sacred Wisps\nMaximum 2 Sacred Wisps", + "Sacred Wisps have 25% chance to use the Triggering Skill when you fire a\nProjectile with that Skill", + "Sacred Wisps have +25% chance to use the Triggering Skill when you fire a Projectile with that Skill while a Rare or Unique Enemy is in your Presence" + ], + "secDescrText": "This skill is triggered by supported skills to summon Sacred Wisps which use the triggering skill when you do." + } + } + ] + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvUmluZ3MvS2lrYXphcnUiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwibXV0YXRlZCI6dHJ1ZX1d/709da29b75/Kikazaru.png", + "league": "Mirage", + "id": "168e40c57f9f51eef47d7d0f47db40e84b7d956606e63e1604dacc0b3cd03852", + "mutated": true, + "name": "Foulborn Kikazaru", + "typeLine": "Topaz Ring", + "baseType": "Topaz Ring", + "rarity": "Unique", + "ilvl": 83, + "identified": true, + "requirements": [ + { + "name": "Level", + "values": [["20", 0]], + "displayMode": 0, + "type": 62 + } + ], + "implicitMods": ["+27% to Lightning Resistance"], + "explicitMods": [ + "+11 to all Attributes", + "36% increased Mana Regeneration Rate", + "60% reduced Effect of Curses on you" + ], + "mutatedMods": ["+2 Maximum Mana per Level"], + "flavourText": ["Hear no evil."], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "Ring2" + }, + { + "verified": false, + "w": 2, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQXJtb3Vycy9Cb290cy9Cb290c0ludDQiLCJ3IjoyLCJoIjoyLCJzY2FsZSI6MSwic2VhcmluZyI6dHJ1ZSwidGFuZ2xlZCI6dHJ1ZSwiZnJhY3R1cmVkIjp0cnVlfV0/2b8249ac60/BootsInt4.png", + "league": "Mirage", + "id": "08ec4d17987dd633742d89ff6ce9e0cc6f1871e825140e709abd45efc328b21c", + "searing": true, + "tangled": true, + "fractured": true, + "sockets": [ + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 1, + "attr": "D", + "sColour": "G" + }, + { + "group": 1, + "attr": "D", + "sColour": "G" + } + ], + "name": "Fate Dash", + "typeLine": "Warlock Boots", + "baseType": "Warlock Boots", + "rarity": "Rare", + "ilvl": 84, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Energy Shield", + "values": [["184", 1]], + "displayMode": 0, + "type": 18 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["84", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["130", 0]], + "displayMode": 1, + "type": 64, + "suffix": "(gem)" + }, + { + "name": "Int", + "values": [["177", 0]], + "displayMode": 1, + "type": 65 + } + ], + "implicitMods": [ + "Gain 5% of Physical Damage as Extra Cold Damage", + "4% increased Action Speed" + ], + "explicitMods": [ + "+57 to Intelligence", + "+16 to maximum Mana", + "+39% to Cold Resistance", + "+38% to Lightning Resistance" + ], + "craftedMods": [ + "20% increased Movement Speed", + "100% chance to Avoid being Chilled" + ], + "fracturedMods": ["97% increased Energy Shield"], + "frameType": 2, + "x": 0, + "y": 0, + "inventoryId": "Boots", + "socketedItems": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9DaXJjbGVvZlBvd2VyU2tpbGxHZW0iLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/7b2960c51a/CircleofPowerSkillGem.png", + "support": false, + "league": "Mirage", + "id": "940a1158617d9decc2fcc23b443e8b7c49b518ad6fbbcb9971e72917410b2f01", + "name": "", + "typeLine": "Sigil of Power", + "baseType": "Sigil of Power", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Spell, AoE, Duration, Lightning, Arcane", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["54 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["10.00 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["0.50 sec", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+23%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["155", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Places a Sigil on the ground, which grants a buff to you and allies in the area around it for a duration. The Sigil gains stages as you spend mana in its area, making the buff more powerful. You can only have one Sigil of Power at a time.", + "explicitMods": [ + "Base duration is 12.00 seconds", + "Gains a Stage when you Spend a total of 400 Mana while in Area", + "Enemies in Area deal 23% less Damage while at maximum Stages", + "Maximum 4 Stages", + "Buff grants 7 to 141 Added Lightning Damage per Stage", + "Minimum 1 second between Stages" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 1, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9Qcm9qZWN0aWxlV2Vha25lc3MiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/92040afb2a/ProjectileWeakness.png", + "support": false, + "league": "Mirage", + "id": "58ef43be25dad3a5ab24575bd5753542fb37a8774c45cd21f33fe0802b65b9d0", + "name": "", + "typeLine": "Sniper's Mark", + "baseType": "Sniper's Mark", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Spell, Curse, Mark", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["16", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["30 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["0.50 sec", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+23%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["58", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["130", 0]], + "displayMode": 1, + "type": 64 + } + ], + "additionalProperties": [ + { + "name": "Experience", + "values": [["13437908/13437908", 0]], + "displayMode": 2, + "progress": 1, + "type": 20 + } + ], + "nextLevelRequirements": [ + { + "name": "Level", + "values": [["61", 0]], + "displayMode": 0 + }, + { + "name": "Dex", + "values": [["136", 0]], + "displayMode": 1 + } + ], + "secDescrText": "Curses a single enemy, increasing the damage they take from projectiles, and making projectiles split when hitting them, to hit other targets around them. You can only have one Mark at a time.", + "explicitMods": [ + "Cursed enemies take 30% increased Damage from Projectile Hits", + "Projectiles which Hit Cursed Enemies Split towards 2 additional targets" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 2, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L01hcmtPbkhpdHRpbmciLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/082becefa3/MarkOnHitting.png", + "support": true, + "league": "Mirage", + "id": "f8a25142831f272a65c741581afb267b73c8fe341e81cb400af760036d6d42d4", + "name": "", + "typeLine": "Mark On Hit Support", + "baseType": "Mark On Hit Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Support, Mark, Trigger", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["200%", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["4.00 sec", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["111", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports mark curse skills.", + "explicitMods": [ + "16% reduced Effect of Marks from Supported Skills", + "Trigger Supported Skill when you Hit a Rare or Unique Enemy with an Attack" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 3, + "colour": "D" + } + ] + }, + { + "verified": false, + "w": 2, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQXJtb3Vycy9HbG92ZXMvR2xvdmVzSW50NCIsInciOjIsImgiOjIsInNjYWxlIjoxLCJzZWFyaW5nIjp0cnVlLCJ0YW5nbGVkIjp0cnVlLCJmcmFjdHVyZWQiOnRydWV9XQ/93b0ea7a6a/GlovesInt4.png", + "league": "Mirage", + "id": "a0d791644c12d7cdec6cd290f710686e861f2085c1a674d24129179fc43616f7", + "searing": true, + "tangled": true, + "fractured": true, + "sockets": [ + { + "group": 0, + "attr": "D", + "sColour": "G" + }, + { + "group": 1, + "attr": "D", + "sColour": "G" + }, + { + "group": 1, + "attr": "D", + "sColour": "G" + }, + { + "group": 1, + "attr": "I", + "sColour": "B" + } + ], + "name": "Plague Touch", + "typeLine": "Sorcerer Gloves", + "baseType": "Sorcerer Gloves", + "rarity": "Rare", + "ilvl": 84, + "identified": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Energy Shield", + "values": [["139", 1]], + "displayMode": 0, + "type": 18 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62, + "suffix": "(gem)" + }, + { + "name": "Dex", + "values": [["119", 0]], + "displayMode": 1, + "type": 64, + "suffix": "(gem)" + }, + { + "name": "Int", + "values": [["155", 0]], + "displayMode": 1, + "type": 65, + "suffix": "(gem)" + } + ], + "implicitMods": [ + "20% of Physical Damage Converted to Lightning Damage", + "Gain 1 Rage on Attack Hit" + ], + "explicitMods": [ + "+46 to Intelligence", + "107% increased Energy Shield", + "+68 to maximum Mana", + "15% increased Stun and Block Recovery" + ], + "craftedMods": ["+34% to Fire Resistance"], + "fracturedMods": ["+47% to Lightning Resistance"], + "frameType": 2, + "x": 0, + "y": 0, + "inventoryId": "Gloves", + "socketedItems": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9BY2N1cmFjeWFuZENyaXRpY2FsQ2hhbmNlQXVyYSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/e8f00e99b6/AccuracyandCriticalChanceAura.png", + "support": false, + "league": "Mirage", + "id": "efbf49cf84e7b845b4bde3d8267f565859a884511811c60967d3b6c3b8202986", + "name": "", + "typeLine": "Precision", + "baseType": "Precision", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Aura, Critical, Spell, AoE", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Reservation", + "values": [["186 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["1.20 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["119", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Casts an aura that grants accuracy and critical strike chance to you and your allies.", + "explicitMods": [ + "+1.9 metres to radius", + "You and nearby allies gain 58% increased Critical Strike Chance", + "You and nearby allies gain +701 to Accuracy Rating" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 0, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0N1bGxpbmdTdHJpa2UiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/328a156075/CullingStrike.png", + "support": true, + "league": "Mirage", + "id": "495e1ac97354595bf402bf874b77402c8fa7da09d9dd05f92a90edd2be8ff5b6", + "name": "", + "typeLine": "Culling Strike Support", + "baseType": "Culling Strike Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Support", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["110%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["111", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports any skill that hits enemies. If enemies are left below 10% of maximum life after being hit by these skills, they will be killed.", + "explicitMods": [ + "Kill Enemies that have 10% Life or lower when Hit by Supported Skills", + "Supported Skills deal 38% increased Damage", + "Recover 2% of Life when Supported Skills Cull an Enemy" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 1, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9Ub3JuYWRvR2VtIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/17ebe5873b/TornadoGem.png", + "support": false, + "league": "Mirage", + "id": "86312ab9c4408c00952fdd62ae630cd691983674204a8e87795c9f47dd34737a", + "name": "", + "typeLine": "Tornado", + "baseType": "Tornado", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Spell, Duration, Physical, AoE, Orb", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["1", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["15 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["0.75 sec", 0]], + "displayMode": 0 + }, + { + "name": "Critical Strike Chance", + "values": [["5.00%", 0]], + "displayMode": 0 + }, + { + "name": "Effectiveness of Added Damage", + "values": [["55%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["34", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["50", 0]], + "displayMode": 1, + "type": 64 + }, + { + "name": "Int", + "values": [["35", 0]], + "displayMode": 1, + "type": 65 + } + ], + "additionalProperties": [ + { + "name": "Experience", + "values": [["252595/252595", 0]], + "displayMode": 2, + "progress": 1, + "type": 20 + } + ], + "nextLevelRequirements": [ + { + "name": "Level", + "values": [["36", 0]], + "displayMode": 0 + }, + { + "name": "Dex", + "values": [["53", 0]], + "displayMode": 1 + }, + { + "name": "Int", + "values": [["37", 0]], + "displayMode": 1 + } + ], + "secDescrText": "Create a Tornado that hinders and repeatedly damages enemies around it. It will move forward for a duration, during which your projectiles can collide with it to deal damage as though it was an enemy. Then it will chase down enemies for a secondary duration, and reflect a portion of the damage it took from your projectiles to them in addition to its own damage.", + "explicitMods": [ + "Deals 31 to 46 Physical Damage", + "Deals Damage every 0.25 seconds", + "Base duration is 1.50 seconds", + "Base secondary duration is 4.00 seconds", + "During initial Duration, can be hit by your Projectiles up to 20 times", + "Reflects 10% of Damage Taken to Enemies", + "Enemies in range are Hindered", + "Maximum 1 Tornado", + "Tornado has 20% increased Movement Speed" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 2, + "colour": "D" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TcGVsbHNsaW5nZXIiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/56a197623f/Spellslinger.png", + "support": false, + "league": "Mirage", + "id": "3bcebc02376615053c50b5b842283a7fae73cc230444e6d0977caf8884e31083", + "name": "", + "typeLine": "Spellslinger Support", + "baseType": "Spellslinger", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Trigger, Spell", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cooldown Time", + "values": [["0.60 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["155", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Reserves mana based on the supported spells to cause those spells to trigger when you fire projectiles from a wand attack.", + "explicitMods": [ + "This Skill's Mana Reservation is the total of the Mana Reservations of Supported Skills, and cannot be further modified" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 3, + "colour": "I", + "hybrid": { + "baseTypeName": "Spellslinger", + "properties": [ + { + "name": "Reservation Override", + "values": [["20% Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["0.60 sec", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "Trigger Supported Spells when you fire Projectiles from a Non-Triggered Wand Attack", + "Supported Skills have 38% increased Cooldown Recovery Rate", + "Supported Skills have Added Spell Damage equal to 194% of Damage of Equipped Wand\nIf two Wands are Equipped, each contributes half as much Added Damage", + "Supported Skills deal 20% less Damage with Hits and Ailments" + ], + "secDescrText": "Supports spell skills that have no reservation. Cannot support skills used by totems, traps or mines. Cannot modify the skills of minions." + } + } + ] + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQW11bGV0cy9UdXJxdW9pc2VBbXVsZXQiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwiZnJhY3R1cmVkIjp0cnVlfV0/7e80887e14/TurquoiseAmulet.png", + "league": "Mirage", + "id": "cc389879c131e5459a30935bd564c8fbec71ee93d5c7aad99c6dd714af304761", + "fractured": true, + "name": "Oblivion Locket", + "typeLine": "Turquoise Amulet", + "baseType": "Turquoise Amulet", + "rarity": "Rare", + "ilvl": 83, + "identified": true, + "properties": [ + { + "name": "Quality (Attribute Modifiers)", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["65", 0]], + "displayMode": 0, + "type": 62 + } + ], + "enchantMods": ["Allocates Primal Spirit"], + "implicitMods": ["+28 to Dexterity and Intelligence"], + "explicitMods": [ + "+39% to Global Critical Strike Multiplier", + "20% increased maximum Energy Shield", + "+55 to maximum Mana", + "+39% to Cold Resistance" + ], + "craftedMods": ["Non-Channelling Skills have -7 to Total Mana Cost"], + "fracturedMods": ["+62 to Intelligence"], + "frameType": 2, + "x": 0, + "y": 0, + "inventoryId": "Amulet" + }, + { + "verified": false, + "w": 2, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQXJtb3Vycy9IZWxtZXRzL1ViZXJFbGRlckhlbG1ldCIsInciOjIsImgiOjIsInNjYWxlIjoxfV0/005bcc2179/UberElderHelmet.png", + "league": "Mirage", + "id": "a119f52c991506e4a8b4b48cd7696f9101099247393f42f2ebf52b6efeb10a39", + "influences": { + "shaper": true, + "elder": true + }, + "elder": true, + "shaper": true, + "sockets": [ + { + "group": 0, + "attr": "S", + "sColour": "R" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + } + ], + "name": "Indigon", + "typeLine": "Hubris Circlet", + "baseType": "Hubris Circlet", + "rarity": "Unique", + "ilvl": 86, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Energy Shield", + "values": [["294", 1]], + "displayMode": 0, + "type": 18 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["76", 0]], + "displayMode": 0, + "type": 62, + "suffix": "(gem)" + }, + { + "name": "Str", + "values": [["111", 0]], + "displayMode": 1, + "type": 63, + "suffix": "(gem)" + }, + { + "name": "Dex", + "values": [["68", 0]], + "displayMode": 1, + "type": 64, + "suffix": "(gem)" + }, + { + "name": "Int", + "values": [["159", 0]], + "displayMode": 1, + "type": 65, + "suffix": "(gem)" + } + ], + "explicitMods": [ + "202% increased Energy Shield", + "8% increased maximum Mana", + "Recover 10% of Life when you use a Mana Flask", + "Non-instant Mana Recovery from Flasks is also Recovered as Life", + "45% increased Cost of Skills for each 200 total Mana Spent Recently", + "21% increased Spell Damage for each 200 total Mana you have Spent Recently, up to 2000%" + ], + "flavourText": [ + "Where the body's limits begin,\r", + "the mind's limits end." + ], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "Helm", + "socketedItems": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0luY3JlYXNlZER1cmF0aW9uIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/a5db7ae0bd/IncreasedDuration.png", + "support": true, + "league": "Mirage", + "id": "bcf9457531c59c9eb201cb7833c953c7671a9fb76728be23c7bf6e29f245421d", + "name": "", + "typeLine": "More Duration Support", + "baseType": "More Duration Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Support, Duration", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["130%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Str", + "values": [["111", 0]], + "displayMode": 1, + "type": 63 + } + ], + "secDescrText": "Supports any skill with a duration.", + "explicitMods": [ + "Supported Skills have 49% more Skill Effect Duration" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "socket": 0, + "colour": "S" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0Nvb2xkb3duUmVjb3ZlcnkiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwiZ2UiOnRydWV9XQ/21c017b506/CooldownRecovery.png", + "support": true, + "league": "Mirage", + "id": "94087cf79fd4a92d847fd77434eaec6cae878058266a4d39cc9c7b68b0a039b0", + "name": "", + "typeLine": "Cooldown Recovery Support", + "baseType": "Cooldown Recovery Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Exceptional, Support", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["3 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["150%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["76", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["52", 0]], + "displayMode": 1, + "type": 64 + }, + { + "name": "Int", + "values": [["75", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Supports skills that have a cooldown. Cannot modify the skills of minions.", + "explicitMods": [ + "Supported Skills have 45% increased Cooldown Recovery Rate" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "flavourText": [ + "\"The science is... incomprehensible at best. Nearest I can tell, the Arcana exhibits\r", + "a blast of wrath once it has been fully charged, and when directed at the Elder\r", + "will force it to take on the form it held before it entered our dimension.\"" + ], + "frameType": 4, + "socket": 1, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9BcmNhbmVDbG9hayIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/1183c052ce/ArcaneCloak.png", + "support": false, + "league": "Mirage", + "id": "6da1b7d9a7b539d2bd525e78abd62bf5b5e5ce45bf6c12c580cb6502edaeded5", + "name": "", + "typeLine": "Arcane Cloak", + "baseType": "Arcane Cloak", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Spell, Duration, Guard, Lightning, Arcane", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["21 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cooldown Time", + "values": [["4.00 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["72", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["159", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Spends a portion of your mana to grant a buff that takes some of the damage from hits for you until depleted. The buff grants added lightning damage based on the amount of mana spent by this skill. Shares a cooldown with other Guard skills.", + "explicitMods": [ + "Base duration is 3.00 seconds", + "Spends 65% of current Mana", + "75% of Damage from Hits is taken from Buff before your Life or Energy Shield\nBuff can take Damage equal to Mana Spent by this Skill's effect", + "Buff grants Added Lightning Damage equal to 15% of Mana Spent by this Skill's effect", + "This Skill's Cooldown does not recover during its effect", + "20% increased Buff Effect" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 2, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9BdXRvQ2FzdEluc3RhbnRTa2lsbHMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/ec5d94a83e/AutoCastInstantSkills.png", + "support": false, + "league": "Mirage", + "id": "7d25b8d17279e6ce5bee04f98ed3f2de49e1710af5e6b83da1477935854610f2", + "name": "", + "typeLine": "Automation Support", + "baseType": "Automation", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Trigger, Spell", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cooldown Time", + "values": [["0.60 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["68", 0]], + "displayMode": 1, + "type": 64 + }, + { + "name": "Int", + "values": [["98", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "While this skill is active, supported spells will be repeatedly triggered.", + "explicitMods": [ + "Each Supported Spell will Trigger when its Cooldown is over" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 3, + "colour": "I", + "hybrid": { + "baseTypeName": "Automation", + "properties": [ + { + "name": "Cost & Reservation Multiplier", + "values": [["150%", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "Supported Spells are Triggered when their Cooldowns are over" + ], + "secDescrText": "Supports spell skills that are instant and have no reservation. Cannot support Vaal skills or skills used by totems, traps or mines. Cannot modify the skills of minions." + } + } + ] + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvUmluZ3MvSGVpc3RSaW5nMURhcmsiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwiZnJhY3R1cmVkIjp0cnVlfV0/e8a74e9da2/HeistRing1Dark.png", + "league": "Mirage", + "id": "351417234afa9deb15577c779309b095e23e415a066fda5ae7d6ba7e7157de42", + "fractured": true, + "name": "Oblivion Gyre", + "typeLine": "Helical Ring", + "baseType": "Helical Ring", + "rarity": "Rare", + "ilvl": 85, + "identified": true, + "split": true, + "requirements": [ + { + "name": "Level", + "values": [["67", 0]], + "displayMode": 0, + "type": 62 + } + ], + "implicitMods": [ + "-2 Prefix Modifiers allowed", + "+1 Suffix Modifier allowed", + "Implicit Modifiers Cannot Be Changed", + "50% increased Suffix Modifier magnitudes" + ], + "explicitMods": [ + "+76 to Intelligence", + "+36% to Global Critical Strike Multiplier", + "+70% to Cold Resistance" + ], + "craftedMods": [ + "+40 to maximum Mana", + "5% reduced Mana Cost of Skills" + ], + "fracturedMods": ["+72% to Fire Resistance"], + "frameType": 2, + "x": 0, + "y": 0, + "inventoryId": "Ring" + }, + { + "verified": false, + "w": 1, + "h": 3, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvV2VhcG9ucy9PbmVIYW5kV2VhcG9ucy9XYW5kcy9VYmVyTWF2ZW5XYW5kIiwidyI6MSwiaCI6Mywic2NhbGUiOjF9XQ/1e06b23a5b/UberMavenWand.png", + "league": "Mirage", + "id": "c46e3064e2af55830b036d1ec57edff724fe0d5c0dc7ead6b5810782dcce7b78", + "sockets": [ + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + } + ], + "name": "Grace of the Goddess", + "typeLine": "Prophecy Wand", + "baseType": "Prophecy Wand", + "rarity": "Unique", + "ilvl": 87, + "identified": true, + "properties": [ + { + "name": "Wand", + "values": [], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Physical Damage", + "values": [["107-197", 1]], + "displayMode": 0, + "type": 9 + }, + { + "name": "Critical Strike Chance", + "values": [["8.70%", 0]], + "displayMode": 0, + "type": 12 + }, + { + "name": "Attacks per Second", + "values": [["1.50", 0]], + "displayMode": 0, + "type": 13 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62, + "suffix": "(gem)" + }, + { + "name": "Str", + "values": [["68", 0]], + "displayMode": 1, + "type": 63, + "suffix": "(gem)" + }, + { + "name": "Int", + "values": [["245", 0]], + "displayMode": 1, + "type": 65 + } + ], + "enchantMods": [ + "Quality does not increase Physical Damage", + "Grants 1% increased Area of Effect per 4% Quality" + ], + "implicitMods": ["40% increased Spell Damage"], + "explicitMods": [ + "310% increased Physical Damage", + "Gain 46% of Physical Damage as Extra Fire Damage", + "Gain 40% of Physical Damage as Extra Cold Damage", + "Gain 44% of Physical Damage as Extra Lightning Damage", + "+1 to maximum number of Sacred Wisps\n+1 to number of Sacred Wisps Summoned" + ], + "flavourText": [ + "In a time of darkness, know that the Draíocht will bring you light." + ], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "Weapon", + "socketedItems": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9UZW1wZXN0U2hpZWxkIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/df5de3f529/TempestShield.png", + "support": false, + "league": "Mirage", + "id": "0b504ca98b6b9858ded4c4833372d42bd64665279eb8ca3cf2b486e176d14abc", + "name": "", + "typeLine": "Tempest Shield", + "baseType": "Tempest Shield", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Spell, Lightning, Chaining", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Reservation", + "values": [["25% Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["1.00 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + }, + { + "name": "Critical Strike Chance", + "values": [["6.00%", 0]], + "displayMode": 0 + }, + { + "name": "Effectiveness of Added Damage", + "values": [["220%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Str", + "values": [["68", 0]], + "displayMode": 1, + "type": 63 + }, + { + "name": "Int", + "values": [["98", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Enchants your shield with the power of storms, which lashes out to deal arcing lightning damage to attackers when you block them.", + "explicitMods": [ + "Deals 896 to 2688 Lightning Damage", + "Chains +1 Times", + "40% chance to Shock enemies", + "Buff grants +25% Chance to Block Spell Damage while holding a Shield", + "Buff grants Immunity to Shock" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 0, + "colour": "I" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9WYWFsR2Vtcy9WYWFsQ2xhcml0eSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/e480915860/VaalClarity.png", + "support": false, + "league": "Mirage", + "id": "491170b9a9f0b90fec9411942e22e1699ebd00a3cedb3e9f0a0e9cf7e5df07e1", + "name": "", + "typeLine": "Vaal Clarity", + "baseType": "Vaal Clarity", + "ilvl": 0, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Aura, Spell, AoE, Duration, Vaal", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["1", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Reservation", + "values": [["34 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["1.20 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["10", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["22", 0]], + "displayMode": 1, + "type": 65 + } + ], + "additionalProperties": [ + { + "name": "Experience", + "values": [["9569/9569", 0]], + "displayMode": 2, + "progress": 1, + "type": 20 + } + ], + "nextLevelRequirements": [ + { + "name": "Level", + "values": [["13", 0]], + "displayMode": 0 + }, + { + "name": "Int", + "values": [["27", 0]], + "displayMode": 1 + } + ], + "secDescrText": "Casts an aura that grants mana regeneration to you and your allies.", + "explicitMods": [ + "You and nearby Allies Regenerate 2.9 Mana per second" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 1, + "colour": "I", + "hybrid": { + "isVaalGem": true, + "baseTypeName": "Clarity", + "properties": [ + { + "name": "Cooldown Time", + "values": [["0.50 sec", 0]], + "displayMode": 0 + }, + { + "name": "Souls Per Use", + "values": [["50", 0]], + "displayMode": 0 + }, + { + "name": "Can Store {0} Use", + "values": [["1", 0]], + "displayMode": 3 + }, + { + "name": "Soul Gain Prevention", + "values": [["14 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "Base duration is 10.00 seconds", + "Modifiers to Buff Duration also apply to this Skill's Soul Gain Prevention", + "Your and nearby allies' Skills Cost no Mana" + ], + "secDescrText": "Casts a temporary aura that lets you and your allies cast skills without paying their mana costs." + } + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9Gcm9zdGJsaW5rU2tpbGxHZW0iLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/03a84470ab/FrostblinkSkillGem.png", + "support": false, + "league": "Mirage", + "id": "0d2ab3917e3a51939af4c99b393bbd196818c63d8c3b6f2848d7c67ac84743a2", + "name": "", + "typeLine": "Frostblink", + "baseType": "Frostblink", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Spell, Movement, Duration, Cold, Travel, Blink, AoE", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["22 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Cooldown Time", + "values": [["2.60 sec", 0]], + "displayMode": 0 + }, + { + "name": "Cast Time", + "values": [["Instant", 0]], + "displayMode": 0 + }, + { + "name": "Critical Strike Chance", + "values": [["5.00%", 0]], + "displayMode": 0 + }, + { + "name": "Effectiveness of Added Damage", + "values": [["250%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Int", + "values": [["155", 0]], + "displayMode": 1, + "type": 65 + } + ], + "secDescrText": "Teleport to a location, damaging enemies and leaving Chilled ground in an area at both ends of the teleport. Shares a cooldown with other Blink skills.", + "explicitMods": [ + "Deals 1144 to 1716 Cold Damage", + "Base duration is 3.00 seconds", + "+0.3 metres to radius", + "19% increased Cooldown Recovery Rate for each Normal or Magic Enemy in Area\n99% increased Cooldown Recovery Rate for each Rare or Unique Enemy in Area", + "48% increased maximum travel distance" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "socket": 2, + "colour": "I" + } + ] + } + ], + "inventory": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL1VuaXF1ZUpld2VsQmFzZTEiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/166fecc863/UniqueJewelBase1.png", + "league": "Mirage", + "id": "877f534aafb217c12eb6ecdeb3b814f6d0e2b61447074694438ffdfe26f4bc4c", + "name": "Split Personality", + "typeLine": "Crimson Jewel", + "baseType": "Crimson Jewel", + "rarity": "Unique", + "ilvl": 79, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Limited to", + "values": [["2", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "This Jewel's Socket has 25% increased effect per Allocated Passive Skill between\nit and your Class' starting location", + "+5 to maximum Energy Shield", + "+5 to maximum Mana" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": ["You need not go looking for a second opinion."], + "frameType": 3, + "x": 1, + "y": 0, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L0Zhc3RlckF0dGFja3MiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/c3e1544a95/FasterAttacks.png", + "support": true, + "league": "Mirage", + "id": "eb22121089601d4296924cb6573ccafe2dc3df3ce3d5bbb7e0c0882f6d1e27c6", + "name": "", + "typeLine": "Faster Attacks Support", + "baseType": "Faster Attacks Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Attack, Support", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["110%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["111", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports attack skills.", + "explicitMods": ["Supported Skills have 54% increased Attack Speed"], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "x": 10, + "y": 3, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzMwLDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TaGllbGRDaGFyZ2UiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/6d2723f822/ShieldCharge.png", + "support": false, + "league": "Mirage", + "id": "97fa1e603f97caec71296bc48d1e19bbe86c5526ece5f5a6a7991ee8ba58c3b6", + "name": "", + "typeLine": "Shield Charge", + "baseType": "Shield Charge", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Attack, Melee, AoE, Movement, Travel, Physical", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["15", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost", + "values": [["9 Mana", 0]], + "displayMode": 0 + }, + { + "name": "Attack Time", + "values": [["0.50 sec", 0]], + "displayMode": 0 + }, + { + "name": "Critical Strike Chance", + "values": [["5.00%", 0]], + "displayMode": 0 + }, + { + "name": "Attack Damage", + "values": [["100% of base", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["55", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Str", + "values": [["123", 0]], + "displayMode": 1, + "type": 63 + } + ], + "additionalProperties": [ + { + "name": "Experience", + "values": [["7611351/7611351", 0]], + "displayMode": 2, + "progress": 1, + "type": 20 + } + ], + "nextLevelRequirements": [ + { + "name": "Level", + "values": [["58", 0]], + "displayMode": 0 + }, + { + "name": "Str", + "values": [["130", 0]], + "displayMode": 1 + } + ], + "secDescrText": "Charges at a targeted location or enemy, pushing away enemies in your path and repeatedly dealing off-hand damage in a small area in front of you. You deal damage in a larger area when you reach the target. The further you travel, the more damage you deal, and the greater your chance of stunning enemies.", + "explicitMods": [ + "142 to 213 Base Off Hand Physical Damage", + "6 to 10 Added Physical Damage per 15 Armour or Evasion Rating on Shield", + "75% increased Stun Threshold reduction on enemies at Maximum charge distance", + "100% more Damage with Hits at Maximum Charge Distance", + "124% increased Movement Speed" + ], + "descrText": "Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.", + "frameType": 4, + "x": 10, + "y": 4, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lJZGVudGlmaWNhdGlvbiIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/3a1e615322/CurrencyIdentification.png", + "stackSize": 37, + "maxStackSize": 40, + "league": "Mirage", + "id": "c0b127812caef6a1bdaafe1a1bb806688c8800da0459739f43a1904e9e255ea3", + "name": "", + "typeLine": "Scroll of Wisdom", + "baseType": "Scroll of Wisdom", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Stack Size", + "values": [["37/40", 0]], + "displayMode": 0, + "type": 32 + } + ], + "explicitMods": ["Identifies an item"], + "descrText": "Right click this item then left click an unidentified item to apply it.", + "frameType": 5, + "x": 11, + "y": 3, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lQb3J0YWwiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/a55c72f398/CurrencyPortal.png", + "stackSize": 32, + "maxStackSize": 40, + "league": "Mirage", + "id": "0cc916b9d11bde7bb29a185a94e1efe73201bdfbf9dea28d58370ed07bcb8dcb", + "name": "", + "typeLine": "Portal Scroll", + "baseType": "Portal Scroll", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Stack Size", + "values": [["32/40", 0]], + "displayMode": 0, + "type": 32 + } + ], + "explicitMods": ["Creates a portal to town"], + "descrText": "Right click on this item to use it.", + "frameType": 5, + "x": 11, + "y": 4, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvR2Vtcy9TdXBwb3J0L01vbWVudHVtU3VwcG9ydEdlbSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/ccdd57e05a/MomentumSupportGem.png", + "support": true, + "league": "Mirage", + "id": "4de16cad2c362606e07ce6a8ad1d23aa141b6481a101ea81577f8b7bf052f8fa", + "name": "", + "typeLine": "Momentum Support", + "baseType": "Momentum Support", + "ilvl": 0, + "identified": true, + "properties": [ + { + "name": "Attack, Support, Duration", + "values": [], + "displayMode": 0 + }, + { + "name": "Level", + "values": [["20 (Max)", 0]], + "displayMode": 0, + "type": 5 + }, + { + "name": "Cost & Reservation Multiplier", + "values": [["110%", 0]], + "displayMode": 0 + }, + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["70", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Str", + "values": [["48", 0]], + "displayMode": 1, + "type": 63 + }, + { + "name": "Dex", + "values": [["70", 0]], + "displayMode": 1, + "type": 64 + } + ], + "secDescrText": "Supports attack skills that aren't triggered.", + "explicitMods": [ + "Gain 1 Momentum when you Use a Supported Skill\nGain 1 Momentum every 0.51 seconds while Channelling a Supported Skill\nLose all Momentum if you Move", + "Supported Skills have 20% increased Attack Speed per Momentum", + "When you reach 5 Momentum, lose all Momentum and gain\nSwiftness for 2.0 seconds", + "Swiftness grants 15% increased Movement Speed per Momentum lost" + ], + "descrText": "This is a Support Gem. It does not grant a bonus to your character, but to skills in sockets connected to it. Place into an item socket connected to a socket containing the Skill Gem you wish to augment. Right click to remove from a socket.", + "frameType": 4, + "x": 9, + "y": 4, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL1VuaXF1ZUpld2VsQmFzZTEiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/166fecc863/UniqueJewelBase1.png", + "league": "Mirage", + "id": "fd4ebc9e7d7e79ef89deb2db1a20df78117c98f2a95fef6b57c88d388ddc8f21", + "name": "Split Personality", + "typeLine": "Crimson Jewel", + "baseType": "Crimson Jewel", + "rarity": "Unique", + "ilvl": 84, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Limited to", + "values": [["2", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "This Jewel's Socket has 25% increased effect per Allocated Passive Skill between\nit and your Class' starting location", + "+5 to maximum Energy Shield", + "+5 to maximum Mana" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": ["You need not go looking for a second opinion."], + "frameType": 3, + "x": 0, + "y": 0, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL05ld0dlbUJhc2UzIiwidyI6MSwiaCI6MSwic2NhbGUiOjEsImZyYWN0dXJlZCI6dHJ1ZX1d/a68dec0dae/NewGemBase3.png", + "league": "Mirage", + "id": "fe7a74dbf7adc125d884d31d4dd442ee49e6fe7c0a121afae1bb226e25a77d72", + "fractured": true, + "name": "Dusk Desire", + "typeLine": "Large Cluster Jewel", + "baseType": "Large Cluster Jewel", + "rarity": "Rare", + "ilvl": 84, + "identified": true, + "requirements": [ + { + "name": "Level", + "values": [["40", 0]], + "displayMode": 0, + "type": 62 + } + ], + "enchantMods": [ + "Adds 8 Passive Skills", + "2 Added Passive Skills are Jewel Sockets", + "Added Small Passive Skills grant: 12% increased Attack Damage while holding a Shield" + ], + "explicitMods": [ + "Added Small Passive Skills also grant: +2% to Fire Resistance", + "1 Added Passive Skill is Heavy Hitter", + "1 Added Passive Skill is Prodigious Defence" + ], + "fracturedMods": ["1 Added Passive Skill is Storm Drinker"], + "descrText": "Place into an allocated Large Jewel Socket on the Passive Skill Tree. Added passives do not interact with jewel radiuses. Right click to remove from the Socket.", + "frameType": 2, + "x": 2, + "y": 0, + "inventoryId": "MainInventory" + }, + { + "verified": false, + "w": 2, + "h": 2, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQXJtb3Vycy9IZWxtZXRzL1RodW5kZXJpbmdXaGlzcGVycyIsInciOjIsImgiOjIsInNjYWxlIjoxfV0/dd96346bfc/ThunderingWhispers.png", + "league": "Mirage", + "id": "24535bf5a222be48f4a6919a9a27eb5160ddffca96911964c84571f9eb101d62", + "sockets": [ + { + "group": 0, + "attr": "S", + "sColour": "R" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + }, + { + "group": 0, + "attr": "I", + "sColour": "B" + } + ], + "name": "Mind of the Council", + "typeLine": "Harlequin Mask", + "baseType": "Harlequin Mask", + "rarity": "Unique", + "ilvl": 83, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Quality", + "values": [["+20%", 1]], + "displayMode": 0, + "type": 6 + }, + { + "name": "Evasion Rating", + "values": [["773", 1]], + "displayMode": 0, + "type": 17 + }, + { + "name": "Energy Shield", + "values": [["241", 1]], + "displayMode": 0, + "type": 18 + } + ], + "requirements": [ + { + "name": "Level", + "values": [["57", 0]], + "displayMode": 0, + "type": 62 + }, + { + "name": "Dex", + "values": [["64", 0]], + "displayMode": 1, + "type": 64 + }, + { + "name": "Int", + "values": [["64", 0]], + "displayMode": 1, + "type": 65 + } + ], + "implicitMods": ["+1 to Maximum Power Charges"], + "explicitMods": [ + "258% increased Evasion and Energy Shield", + "+19 to maximum Energy Shield", + "30% increased maximum Mana", + "10% chance to Shock", + "+20% chance to be Shocked", + "30% of Lightning Damage is taken from Mana before Life", + "Attack Skills have Added Lightning Damage equal to 6% of maximum Mana", + "Lose 3% of Mana when you use an Attack Skill" + ], + "flavourText": [ + "You think we do not know.\r", + "We know all that you think." + ], + "frameType": 3, + "x": 10, + "y": 1, + "inventoryId": "MainInventory", + "socketedItems": [] + } + ], + "jewels": [ + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL0Nvbm5lY3RlZEpld2VsIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/1d2c1f698a/ConnectedJewel.png", + "league": "Mirage", + "id": "71100595860468d2265b8b30e9ba5f7fb6cdaccedaa27817b9f0e0f8f367303e", + "name": "Thread of Hope", + "typeLine": "Crimson Jewel", + "baseType": "Crimson Jewel", + "rarity": "Unique", + "ilvl": 86, + "identified": true, + "properties": [ + { + "name": "Radius", + "values": [["Variable", 1]], + "displayMode": 0, + "type": 24 + } + ], + "explicitMods": [ + "Only affects Passives in Very Large Ring", + "Passive Skills in Radius can be Allocated without being connected to your tree\n-17% to all Elemental Resistances\nPassage" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": [ + "Though we cannot touch; one thought, one wish, through centuries alone in darkness." + ], + "frameType": 3, + "x": 3, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL0FmZmxpY3Rpb25KZXdlbCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/0794886e1c/AfflictionJewel.png", + "league": "Mirage", + "id": "be16b8fe5e21711c5d2717d9b57dcc81851b69b28feabe38d092fb5db9b56d5e", + "name": "The Light of Meaning", + "typeLine": "Prismatic Jewel", + "baseType": "Prismatic Jewel", + "rarity": "Unique", + "ilvl": 85, + "identified": true, + "properties": [ + { + "name": "Limited to", + "values": [["1", 0]], + "displayMode": 0 + }, + { + "name": "Radius", + "values": [["Large", 0]], + "displayMode": 0, + "type": 24 + } + ], + "explicitMods": [ + "Passive Skills in Radius also grant 3% increased Energy Shield" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": ["To name a thing is to give it power."], + "frameType": 3, + "x": 8, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL0JsdWVKZXdlbDciLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/73584bc29e/BlueJewel7.png", + "league": "Mirage", + "id": "6250ee780b1fb6c9b3c814e38452b0639b76637ce17a0b2cc14a2446f4d3e680", + "name": "Healthy Mind", + "typeLine": "Cobalt Jewel", + "baseType": "Cobalt Jewel", + "rarity": "Unique", + "ilvl": 83, + "identified": true, + "corrupted": true, + "properties": [ + { + "name": "Limited to", + "values": [["1", 0]], + "displayMode": 0 + }, + { + "name": "Radius", + "values": [["Large", 0]], + "displayMode": 0, + "type": 24 + } + ], + "explicitMods": [ + "20% increased maximum Mana", + "Increases and Reductions to Life in Radius are Transformed to apply to Mana at 200% of their value" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": ["For the ambitious, flesh is a limitation."], + "frameType": 3, + "x": 14, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL05ld0dlbUJhc2UxIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/0eb1a9d981/NewGemBase1.png", + "league": "Mirage", + "id": "9fa53c41292c7380ec7131e7556ef0be860b64868a4f94b91054c49de5381114", + "name": "Fulgent Bliss", + "typeLine": "Small Cluster Jewel", + "baseType": "Small Cluster Jewel", + "rarity": "Rare", + "ilvl": 74, + "identified": true, + "requirements": [ + { + "name": "Level", + "values": [["54", 0]], + "displayMode": 0, + "type": 62 + } + ], + "enchantMods": [ + "Adds 2 Passive Skills", + "Added Small Passive Skills grant: +15% to Fire Resistance" + ], + "explicitMods": [ + "Added Small Passive Skills also grant: +2 to All Attributes", + "Added Small Passive Skills also grant: +4 to Intelligence", + "Added Small Passive Skills also grant: +6 to Maximum Mana", + "1 Added Passive Skill is Dragon Hunter" + ], + "descrText": "Place into an allocated Small, Medium or Large Jewel Socket on the Passive Skill Tree. Added passives do not interact with jewel radiuses. Right click to remove from the Socket.", + "frameType": 2, + "x": 25, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL1RoZVJlZERyZWFtVXBncmFkZSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/69a20aa6e0/TheRedDreamUpgrade.png", + "league": "Mirage", + "id": "fe39716714038cb83a5ce15cfacdc992a4d44d339361b4cd91ebba108eefee63", + "name": "The Red Nightmare", + "typeLine": "Crimson Jewel", + "baseType": "Crimson Jewel", + "rarity": "Unique", + "ilvl": 80, + "identified": true, + "properties": [ + { + "name": "Limited to", + "values": [["1", 0]], + "displayMode": 0 + }, + { + "name": "Radius", + "values": [["Large", 0]], + "displayMode": 0, + "type": 24 + } + ], + "explicitMods": [ + "Gain 6% of Fire Damage as Extra Chaos Damage", + "Passives granting Fire Resistance or all Elemental Resistances in Radius\nalso grant Chance to Block Attack Damage at 50% of its value" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": [ + "We coagulate; a crimson shell\r", + "that suffocates the unworthy." + ], + "frameType": 3, + "x": 19, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL05ld0dlbUJhc2UyIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/78f6bf8356/NewGemBase2.png", + "league": "Mirage", + "id": "5ec5d682a627ef9866da7968482682cae48bf7f85732111807f9d555b80c6cae", + "name": "Maelström Ornament", + "typeLine": "Medium Cluster Jewel", + "baseType": "Medium Cluster Jewel", + "rarity": "Rare", + "ilvl": 81, + "identified": true, + "requirements": [ + { + "name": "Level", + "values": [["54", 0]], + "displayMode": 0, + "type": 62 + } + ], + "enchantMods": [ + "Adds 4 Passive Skills", + "1 Added Passive Skill is a Jewel Socket", + "Added Small Passive Skills grant: 10% increased Damage over Time" + ], + "explicitMods": [ + "Added Small Passive Skills also grant: 2% increased Damage", + "Added Small Passive Skills also grant: +3 to Dexterity", + "Added Small Passive Skills also grant: 5% increased Mana Regeneration Rate", + "1 Added Passive Skill is Circling Oblivion" + ], + "descrText": "Place into an allocated Medium or Large Jewel Socket on the Passive Skill Tree. Added passives do not interact with jewel radiuses. Right click to remove from the Socket.", + "frameType": 2, + "x": 11, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL1JlY2tsZXNzRGVmZW5zZSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/923e6cb6b5/RecklessDefense.png", + "league": "Mirage", + "id": "ab1f85616aa1aca47a95abf12feb7a2ea2d99a9c1f36e65bceeec0361e4a51f6", + "name": "Replica Reckless Defence", + "typeLine": "Cobalt Jewel", + "baseType": "Cobalt Jewel", + "rarity": "Unique", + "ilvl": 84, + "identified": true, + "explicitMods": [ + "+5% Chance to Block Spell Damage", + "+6% Chance to Block Attack Damage", + "+10% chance to be Frozen, Shocked and Ignited" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": [ + "\"Prototype #298 must be contained in a non-conductive glass box at all times.\"" + ], + "replica": true, + "frameType": 3, + "x": 51, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL2Jhc2ljc3RyIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/8129ab1f4d/basicstr.png", + "league": "Mirage", + "id": "e3daf442078772e21afa400a9ddf649b4385df40a64ea517f258c2ca926ea711", + "name": "Sol Shard", + "typeLine": "Crimson Jewel", + "baseType": "Crimson Jewel", + "rarity": "Rare", + "ilvl": 72, + "identified": true, + "corrupted": true, + "implicitMods": [ + "Minions deal 4% increased Damage", + "Corrupted Blood cannot be inflicted on you" + ], + "explicitMods": [ + "8% increased maximum Mana", + "+14% to Fire Resistance", + "6% chance to Impale Enemies on Hit with Attacks" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "frameType": 2, + "x": 1, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL0VsZGVySmV3ZWwiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MSwicmVsaWMiOjEyfV0/1e777876fc/ElderJewel.png", + "league": "Mirage", + "id": "66296e62b5e04c1f46e4c92af573d886566752ff3fab2cdba71bf5233e637bcc", + "isRelic": true, + "foilVariation": 12, + "name": "Watcher's Eye", + "typeLine": "Prismatic Jewel", + "baseType": "Prismatic Jewel", + "rarity": "Unique", + "ilvl": 84, + "identified": true, + "properties": [ + { + "name": "Limited to", + "values": [["1", 0]], + "displayMode": 0 + } + ], + "explicitMods": [ + "6% increased maximum Energy Shield", + "6% increased maximum Life", + "4% increased maximum Mana", + "14% chance to Recover 10% of Mana when you use a Skill while affected by Clarity", + "Damaging Ailments you inflict deal Damage 11% faster while affected by Malevolence" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": [ + "One by one, they stood their ground against a creature \r", + "they had no hope of understanding, let alone defeating,\r", + "and one by one, they became a part of it." + ], + "frameType": 10, + "x": 53, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL1VubmF0dXJhbEluc3RpbmN0IiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/c37fa5c2b6/UnnaturalInstinct.png", + "league": "Mirage", + "id": "f4e6bf2814b2e28b0b4922cdcb1860ea09ae5878688a2ce4dbdd36452684ada1", + "name": "Unnatural Instinct", + "typeLine": "Viridian Jewel", + "baseType": "Viridian Jewel", + "rarity": "Unique", + "ilvl": 84, + "identified": true, + "properties": [ + { + "name": "Limited to", + "values": [["1", 0]], + "displayMode": 0 + }, + { + "name": "Radius", + "values": [["Small", 0]], + "displayMode": 0, + "type": 24 + } + ], + "explicitMods": [ + "Allocated Small Passive Skills in Radius grant nothing", + "Grants all bonuses of Unallocated Small Passive Skills in Radius" + ], + "descrText": "Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.", + "flavourText": [ + "\"I don't know how I know, \r", + "I just know that I know.\"" + ], + "frameType": 3, + "x": 9, + "y": 0, + "inventoryId": "PassiveJewels" + }, + { + "verified": false, + "w": 1, + "h": 1, + "icon": "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvSmV3ZWxzL05ld0dlbUJhc2UzIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/db35e60885/NewGemBase3.png", + "league": "Mirage", + "id": "5d8cfeb3d9400c3d01266594f44305bd90456452a41308103885c043954390b9", + "name": "Kraken Cut", + "typeLine": "Large Cluster Jewel", + "baseType": "Large Cluster Jewel", + "rarity": "Rare", + "ilvl": 83, + "identified": true, + "requirements": [ + { + "name": "Level", + "values": [["54", 0]], + "displayMode": 0, + "type": 62 + } + ], + "enchantMods": [ + "Adds 8 Passive Skills", + "2 Added Passive Skills are Jewel Sockets", + "Added Small Passive Skills grant: 12% increased Lightning Damage" + ], + "explicitMods": [ + "Added Small Passive Skills also grant: +5% to Lightning Resistance", + "1 Added Passive Skill is Paralysis", + "1 Added Passive Skill is Scintillating Idea", + "1 Added Passive Skill is Widespread Destruction" + ], + "descrText": "Place into an allocated Large Jewel Socket on the Passive Skill Tree. Added passives do not interact with jewel radiuses. Right click to remove from the Socket.", + "frameType": 2, + "x": 20, + "y": 0, + "inventoryId": "PassiveJewels" + } + ], + "passives": { + "hashes": [ + 922, 1031, 2913, 3452, 4492, 5296, 6204, 6230, 6770, 7388, 7960, 8302, + 8948, 9567, 10115, 10490, 11046, 11455, 11551, 11688, 12246, 12913, + 13232, 14936, 15405, 16243, 16775, 18033, 19635, 20228, 20528, 20987, + 21033, 21958, 21984, 22315, 22473, 22618, 22637, 22972, 24256, 24362, + 25651, 25714, 26866, 26960, 27163, 27323, 27592, 27659, 27879, 29026, + 29994, 31819, 33310, 33479, 33545, 33631, 33740, 33755, 34906, 35011, + 35556, 36634, 36678, 37671, 38176, 38516, 39521, 39841, 39916, 41026, + 41263, 41387, 42795, 43000, 43061, 44184, 44908, 44924, 45067, 45456, + 45558, 45680, 46092, 46910, 47251, 48423, 48514, 48768, 48778, 48878, + 49605, 49651, 50288, 50472, 50862, 51146, 51219, 51524, 51923, 52031, + 52157, 52502, 52848, 53188, 53279, 53828, 55114, 55230, 56128, 60090, + 60388, 60440, 61419, 63067, 63207, 63447, 63559, 63976 + ], + "hashes_ex": [2, 5, 11, 27, 28, 198, 200, 202, 204, 206], + "mastery_effects": { + "63559": 64875, + "56128": 15133, + "53188": 64642, + "4492": 5356, + "55230": 62252, + "45558": 30612, + "53828": 63280 + }, + "skill_overrides": { + "37999": { + "name": "Tattoo of the Tawhoa Shaman", + "icon": "Art/2DArt/SkillIcons/passives/LifeRecoupNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/TawhoaTribePassiveBG.png", + "isTattoo": true, + "stats": ["5% chance to Poison on Hit"], + "reminderText": [ + "(Poison deals Chaos Damage over time, based on the base Physical and Chaos Damage of the Skill. Multiple instances of Poison stack)" + ] + }, + "26270": { + "name": "Tattoo of the Tawhoa Shaman", + "icon": "Art/2DArt/SkillIcons/passives/LifeRecoupNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/TawhoaTribePassiveBG.png", + "isTattoo": true, + "stats": ["5% chance to Poison on Hit"], + "reminderText": [ + "(Poison deals Chaos Damage over time, based on the base Physical and Chaos Damage of the Skill. Multiple instances of Poison stack)" + ] + }, + "64210": { + "name": "Tattoo of the Tawhoa Shaman", + "icon": "Art/2DArt/SkillIcons/passives/LifeRecoupNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/TawhoaTribePassiveBG.png", + "isTattoo": true, + "stats": ["5% chance to Poison on Hit"], + "reminderText": [ + "(Poison deals Chaos Damage over time, based on the base Physical and Chaos Damage of the Skill. Multiple instances of Poison stack)" + ] + }, + "39916": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + }, + "35556": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + }, + "44908": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + }, + "50862": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + }, + "15405": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + }, + "33740": { + "name": "Tattoo of the Ngamahu Firewalker", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "activeEffectImage": "Art/2DArt/UIImages/InGame/AncestralTrial/PassiveTreeTattoos/NgamahuTribePassiveBG.png", + "isTattoo": true, + "stats": ["+6% to Fire Resistance"] + } + }, + "bandit_choice": "Eramir", + "pantheon_major": "TheBrineKing", + "pantheon_minor": "Yugul", + "jewel_data": { + "3": { + "type": "JewelStr", + "radius": 2400, + "radiusMin": 2040 + }, + "8": { + "type": "JewelPrismatic", + "radius": 1800 + }, + "14": { + "type": "JewelInt", + "radius": 1800 + }, + "25": { + "type": "JewelPassiveTreeExpansionSmall", + "subgraph": { + "groups": { + "expansion_25": { + "proxy": "18361", + "nodes": ["27", "28"], + "x": -5422.56, + "y": -9436.75, + "orbits": [1] + } + }, + "nodes": { + "27": { + "skill": "14412", + "name": "Dragon Hunter", + "icon": "Art/2DArt/SkillIcons/passives/ArmourNotable.png", + "isNotable": true, + "stats": [ + "30% increased Armour", + "+20% to Fire Resistance", + "15% chance to Defend with 200% of Armour" + ], + "reminderText": [ + "(Armour is treated as this percentage of its actual value, only for calculating damage mitigation)" + ], + "group": "expansion_25", + "orbit": 1, + "orbitIndex": 0, + "out": [], + "in": ["28"] + }, + "28": { + "skill": "36551", + "name": "Fire Resistance", + "icon": "Art/2DArt/SkillIcons/passives/FireResistNode.png", + "stats": [ + "+2 to all Attributes", + "+6 to maximum Mana", + "+15% to Fire Resistance", + "+4 to Intelligence" + ], + "group": "expansion_25", + "orbit": 1, + "orbitIndex": 2, + "out": ["27"], + "in": ["16218"] + } + } + } + }, + "19": { + "type": "JewelStr", + "radius": 1800 + }, + "11": { + "type": "JewelPassiveTreeExpansionMedium", + "subgraph": { + "groups": { + "expansion_11": { + "proxy": "43989", + "nodes": ["1000", "11", "2", "5", "8"], + "x": -4708.05, + "y": -8281.17, + "orbits": [3] + } + }, + "nodes": { + "1000": { + "skill": "48978", + "name": "Damage over Time Multiplier Mastery", + "icon": "Art/2DArt/SkillIcons/passives/AltDamageOverTimeMultiplierMastery.png", + "isMastery": true, + "stats": [], + "group": "expansion_11", + "orbit": 0, + "orbitIndex": 0, + "out": [], + "in": [] + }, + "11": { + "skill": "16218", + "name": "Small Jewel Socket", + "icon": "Art/2DArt/SkillIcons/passives/MasteryBlank.png", + "isJewelSocket": true, + "expansionJewel": { + "size": 0, + "index": 0, + "proxy": "18361", + "parent": "48679" + }, + "stats": [], + "group": "expansion_11", + "orbit": 2, + "orbitIndex": 15, + "out": ["2"], + "in": ["8"] + }, + "2": { + "skill": "10355", + "name": "Circling Oblivion", + "icon": "Art/2DArt/SkillIcons/passives/DamageOverTimeNotable.png", + "isNotable": true, + "stats": [ + "25% increased Damage over Time", + "15% increased Duration of Ailments on Enemies" + ], + "reminderText": [ + "(Ailments are Bleeding, Ignited, Scorched, Chilled, Frozen, Brittle, Shocked, Sapped, and Poisoned)" + ], + "group": "expansion_11", + "orbit": 2, + "orbitIndex": 3, + "out": ["5"], + "in": ["11"] + }, + "5": { + "skill": "32169", + "name": "Damage over Time", + "icon": "Art/2DArt/SkillIcons/passives/DamageOverTimeNode.png", + "stats": [ + "2% increased Damage", + "10% increased Damage over Time", + "5% increased Mana Regeneration Rate", + "+3 to Dexterity" + ], + "group": "expansion_11", + "orbit": 2, + "orbitIndex": 7, + "out": ["8", "2"], + "in": ["7960"] + }, + "8": { + "skill": "32169", + "name": "Damage over Time", + "icon": "Art/2DArt/SkillIcons/passives/DamageOverTimeNode.png", + "stats": [ + "2% increased Damage", + "10% increased Damage over Time", + "5% increased Mana Regeneration Rate", + "+3 to Dexterity" + ], + "group": "expansion_11", + "orbit": 2, + "orbitIndex": 11, + "out": ["11"], + "in": ["5"] + } + } + } + }, + "51": { + "type": "JewelInt" + }, + "1": { + "type": "JewelStr" + }, + "53": { + "type": "JewelPrismatic" + }, + "9": { + "type": "JewelDex", + "radius": 960 + }, + "20": { + "type": "JewelPassiveTreeExpansionLarge", + "subgraph": { + "groups": { + "expansion_20": { + "proxy": "18756", + "nodes": [ + "1195", + "195", + "196", + "197", + "198", + "200", + "202", + "204", + "206" + ], + "x": 4887.73, + "y": -8158.76, + "orbits": [3] + } + }, + "nodes": { + "1195": { + "skill": "15015", + "name": "Lightning Damage Mastery", + "icon": "Art/2DArt/SkillIcons/passives/AltMasteryGroupLightning.png", + "isMastery": true, + "stats": [], + "group": "expansion_20", + "orbit": 0, + "orbitIndex": 0, + "out": [], + "in": [] + }, + "195": { + "skill": "49817", + "name": "Lightning Damage", + "icon": "Art/2DArt/SkillIcons/passives/LightningDamagenode.png", + "stats": [ + "12% increased Lightning Damage", + "+5% to Lightning Resistance" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 0, + "out": ["196"], + "in": ["206"] + }, + "196": { + "skill": "31825", + "name": "Paralysis", + "icon": "Art/2DArt/SkillIcons/passives/IncreasedLightningDamage.png", + "isNotable": true, + "stats": [ + "30% increased Lightning Damage", + "10% chance to double Stun Duration", + "Lightning Skills have 10% reduced Enemy Stun Threshold" + ], + "reminderText": [ + "(The Stun Threshold determines how much Damage can Stun something)" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 1, + "out": ["197"], + "in": ["195"] + }, + "197": { + "skill": "49817", + "name": "Lightning Damage", + "icon": "Art/2DArt/SkillIcons/passives/LightningDamagenode.png", + "stats": [ + "12% increased Lightning Damage", + "+5% to Lightning Resistance" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 3, + "out": ["198"], + "in": ["196"] + }, + "198": { + "skill": "13170", + "name": "Medium Jewel Socket", + "icon": "Art/2DArt/SkillIcons/passives/MasteryBlank.png", + "isJewelSocket": true, + "expansionJewel": { + "size": 1, + "index": 2, + "proxy": "24452", + "parent": "21984" + }, + "stats": [], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 4, + "out": ["200"], + "in": ["197"] + }, + "200": { + "skill": "43834", + "name": "Scintillating Idea", + "icon": "Art/2DArt/SkillIcons/passives/MaxManaNotable.png", + "isNotable": true, + "stats": [ + "20% increased maximum Mana", + "Damage Penetrates 5% Lightning Resistance" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 7, + "out": ["202"], + "in": ["198"] + }, + "202": { + "skill": "49817", + "name": "Lightning Damage", + "icon": "Art/2DArt/SkillIcons/passives/LightningDamagenode.png", + "stats": [ + "12% increased Lightning Damage", + "+5% to Lightning Resistance" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 9, + "out": ["204", "200"], + "in": ["21984"] + }, + "204": { + "skill": "36776", + "name": "Widespread Destruction", + "icon": "Art/2DArt/SkillIcons/passives/IncreasedElementalDamage.png", + "isNotable": true, + "stats": [ + "6% increased Area of Effect", + "20% increased Elemental Damage" + ], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 12, + "out": ["206"], + "in": ["202"] + }, + "206": { + "skill": "64583", + "name": "Medium Jewel Socket", + "icon": "Art/2DArt/SkillIcons/passives/MasteryBlank.png", + "isJewelSocket": true, + "expansionJewel": { + "size": 1, + "index": 0, + "proxy": "58194", + "parent": "21984" + }, + "stats": [], + "group": "expansion_20", + "orbit": 3, + "orbitIndex": 15, + "out": ["195"], + "in": ["204"] + } + } + } + } + }, + "alternate_ascendancy": "Farrul" + }, + "metadata": { + "version": "3.28.0g" + } + } +} \ No newline at end of file diff --git a/spec/System/TestImportReimport_spec.lua b/spec/System/TestImportReimport_spec.lua index c75bfdfc8a..9ae82d037c 100644 --- a/spec/System/TestImportReimport_spec.lua +++ b/spec/System/TestImportReimport_spec.lua @@ -1,5 +1,4 @@ describe("TestImportReimport", function() - local dkjson = require "dkjson" local DEFAULT_CHARACTER_LEVEL = 12 local DEFAULT_ITEM_LEVEL = 10 local TEST_IMPORT_ITEM_ID = "test-import-item-1" @@ -50,18 +49,16 @@ describe("TestImportReimport", function() -- Build a minimal import payload so the tests stay focused on state, not fixture noise. local function buildImportPayload(items) - return dkjson.encode({ - character = { level = DEFAULT_CHARACTER_LEVEL }, - items = items, - }) + return { + level = DEFAULT_CHARACTER_LEVEL, + equipment = items, + } end local function reimportSocketedItemsWithOptions(itemTypeLine, inventoryId, socketedItems, clearItems) - build.importTab.controls.charImportItemsClearSkills.state = true - build.importTab.controls.charImportItemsClearItems.state = clearItems build.importTab:ImportItemsAndSkills(buildImportPayload({ makeImportItem(itemTypeLine, inventoryId, socketedItems), - })) + }), clearItems, true, true) runCallback("OnFrame") end @@ -112,15 +109,13 @@ Added Fire Damage 1/0 DISABLED 1 socketGroup.mainActiveSkill = 2 runCallback("OnFrame") - build.importTab.controls.charImportItemsClearSkills.state = true - build.importTab.controls.charImportItemsClearItems.state = false build.importTab:ImportItemsAndSkills(buildImportPayload({ makeImportItem("Iron Hat", "Helm", { makeSocketedGemEntry(0, false, "Cleave", 1), makeSocketedGemEntry(1, false, "Heavy Strike", 1), makeSocketedGemEntry(2, true, "Added Fire Damage Support", 2), }), - })) + }), false, true, true) runCallback("OnFrame") socketGroup = build.skillsTab.socketGroupList[1] @@ -208,8 +203,6 @@ Blight 20/0 1 glovesGroup.enabled = false runCallback("OnFrame") - build.importTab.controls.charImportItemsClearSkills.state = true - build.importTab.controls.charImportItemsClearItems.state = false build.importTab:ImportItemsAndSkills(buildImportPayload({ makeImportItem("Iron Hat", "Helm", { makeSocketedGemEntry(0, false, "Cleave", 1), @@ -219,7 +212,7 @@ Blight 20/0 1 makeImportItem("Rawhide Gloves", "Gloves", { makeSocketedGemEntry(0, false, "Blight", 20), }, "test-import-item-gloves"), - })) + }), false, true, true) runCallback("OnFrame") local groupsBySlot = {} diff --git a/spec/System/TestImport_spec.lua b/spec/System/TestImport_spec.lua new file mode 100644 index 0000000000..dffd4291f8 --- /dev/null +++ b/spec/System/TestImport_spec.lua @@ -0,0 +1,127 @@ +describe("TestImport", function() + local dkjson = require "dkjson" + + local sampleJson, err = io.open("../spec/System/SampleCharacter.json", "r") + if err then + ConPrintf("Failed to read sample character response: %s", err) + end + local sampleData = dkjson.decode(sampleJson:read("*a")).character + sampleJson:close() + + before_each(function() + newBuild() + end) + + it("imports with correct tree", function() + build.importTab:ImportPassiveTreeAndJewels(sampleData, true) + runCallback("OnFrame") + + assert.equals(build.bandit, "None") + assert.equals(build.characterLevel, 99) + -- iron will and CI + assert.equals(build.spec.allocatedKeystoneCount, 2) + assert.equals(build.spec.curAscendClassName, "Hierophant") + assert.equals(build.spec.curClassName, "Templar") + assert.equals(build.spec.curSecondaryAscendClassName, "Farrul Bloodline") + + -- note: currently only one large cluster and medium is imported on the + -- tree. build actually has a small cluster on the medium cluster + local subGraphLen = 0 + for _ in pairs(build.spec.subGraphs) do + subGraphLen = subGraphLen + 1 + end + + assert.equals(subGraphLen, 2) + assert.equals(build.spec.allocatedMasteryCount, 7) + assert.equals(build.spec.masterySelections[4492], 5356) + + assert.equals(build.spec.allocatedTattooTypes.NgamahuTattoo, 6) + + assert.equals(build.spec.allocatedNotableCount, 29) + end) + it("imports with correct jewels", function() + build.importTab:ImportPassiveTreeAndJewels(sampleData, true) + runCallback("OnFrame") + + local jewelCount = 0 + for _ in pairs(build.spec.jewel_data) do + jewelCount = jewelCount + 1 + end + assert.equals(jewelCount, 11) + assert.equals(build.spec.jewel_data[3].radius, 2400) + assert.equals(build.spec.jewel_data[3].type, "JewelStr") + + local items = build.itemsTab.items + assert.truthy(items) + assert.equals(#items, 11) + + function isEquipped(slot, itemName) + assert.truthy(items[slot]) + assert.equals(items[slot].name, itemName) + local found = false + for k, itemId in pairs(build.spec.jewels) do + if itemId == items[slot].id then + found = true + end + end + assert.truthy(found) + end + isEquipped(3, "Healthy Mind, Cobalt Jewel") + isEquipped(4, "Fulgent Bliss, Small Cluster Jewel") + end) + + function importAndReimportWithOldJewel(shouldDelete) + local oldJewel = new("Item", [[Rarity: RARE +TEST JEWEL +Crimson Jewel +Crafted: true +Prefix: None +Prefix: None +Suffix: None +Suffix: None +Implicits: 0]]) + + build.importTab:ImportPassiveTreeAndJewels(sampleData, true) + + for k,v in pairs(build.itemsTab.sockets) do + if v.label == "Socket #1" then + v:SetSelItemId(0) + end + end + + + build.itemsTab:AddItem(oldJewel, false) + + + -- replace first found jewel + for _, slot in pairs(build.itemsTab.slots) do + if slot.selItemId ~= 0 and slot.nodeId then + slot:SetSelItemId(oldJewel.id) + break + end + end + + build.importTab:ImportPassiveTreeAndJewels(sampleData, shouldDelete) + runCallback("OnFrame") + return oldJewel + end + + it("deletes old jewels", function() + local oldJewel = importAndReimportWithOldJewel(true) + + for _, v in pairs(build.itemsTab.items) do + assert.falsy(v.name == oldJewel.name) + end + end) + + it("doesn't delete old jewels", function() + local oldJewel = importAndReimportWithOldJewel(false) + local found = false + for _, v in pairs(build.itemsTab.items) do + if v.name == oldJewel.name then + found = true + end + end + assert.truthy(found) + end) +end) \ No newline at end of file diff --git a/spec/System/TestTradeQueryCurrency_spec.lua b/spec/System/TestTradeQueryCurrency_spec.lua index d412af2950..7dcdeb8a18 100644 --- a/spec/System/TestTradeQueryCurrency_spec.lua +++ b/spec/System/TestTradeQueryCurrency_spec.lua @@ -5,40 +5,20 @@ describe("TradeQuery Currency Conversion", function() mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) end) - -- test case for commit: "Skip callback on errors to prevent incomplete conversions" - describe("FetchCurrencyConversionTable", function() - -- Pass: Callback not called on error - -- Fail: Callback called, indicating partial data risk - it("skips callback on error", function() - local orig_launch = launch - local spy = { called = false } - launch = { - DownloadPage = function(url, callback, opts) - callback(nil, "test error") - end - } - mock_tradeQuery:FetchCurrencyConversionTable(function() - spy.called = true - end) - launch = orig_launch - assert.is_false(spy.called) - end) - end) - - describe("ConvertCurrencyToChaos", function() - -- Pass: Ceils amount to integer (e.g., 4.9 -> 5) - -- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals + describe("ConvertCurrencyToDivs", function() + -- Pass: Calculates price in divs + -- Fail: Wrong value or nil, indicating broken rounding/baseline logic it("handles chaos currency", function() - mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } } + mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 0.1 } } mock_tradeQuery.pbLeague = "league" - local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9) - assert.are.equal(result, 5) + local result = mock_tradeQuery:ConvertCurrencyToDivs("chaos", 5) + assert.are.equal(result, 0.5) end) -- Pass: Returns nil without crash -- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions it("returns nil for unmapped", function() - local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10) + local result = mock_tradeQuery:ConvertCurrencyToDivs("exotic", 10) assert.is_nil(result) end) end) @@ -46,19 +26,35 @@ describe("TradeQuery Currency Conversion", function() describe("PriceBuilderProcessPoENinjaResponse", function() -- Pass: Processes without error, restoring map while adding a notice -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions - it("handles unmapped currency", function() + it("handles empty response", function() local orig_conv = mock_tradeQuery.currencyConversionTradeMap mock_tradeQuery.currencyConversionTradeMap = { div = "id" } mock_tradeQuery.pbLeague = "league" mock_tradeQuery.pbCurrencyConversion = { league = {} } - mock_tradeQuery.controls.pbNotice = { label = ""} - local resp = { exotic = 10 } - mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp) + mock_tradeQuery.controls.pbNotice = { label = "" } + local resp = { lines = { }} + mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines) -- No crash expected assert.is_true(true) assert.is_true(mock_tradeQuery.controls.pbNotice.label == "No currencies received from PoE Ninja") mock_tradeQuery.currencyConversionTradeMap = orig_conv end) + + -- Pass: Processes without error, restoring map while adding a notice + -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions + it("handles empty response", function() + local orig_conv = mock_tradeQuery.currencyConversionTradeMap + mock_tradeQuery.currencyConversionTradeMap = { div = "id" } + mock_tradeQuery.pbLeague = "league" + mock_tradeQuery.pbCurrencyConversion = { league = {} } + mock_tradeQuery.controls.pbNotice = { label = "" } + local resp = { lines = { { malformedLine = "lol"} }} + mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines) + -- No crash expected + assert.is_true(true) + assert.is_true(mock_tradeQuery.controls.pbNotice.label == "Currencies not updated: malformed PoE Ninja response") + mock_tradeQuery.currencyConversionTradeMap = orig_conv + end) end) describe("GetTotalPriceString", function() diff --git a/spec/System/TestTradeQueryRequests_spec.lua b/spec/System/TestTradeQueryRequests_spec.lua index 873b5102d5..0e731701f8 100644 --- a/spec/System/TestTradeQueryRequests_spec.lua +++ b/spec/System/TestTradeQueryRequests_spec.lua @@ -65,6 +65,42 @@ describe("TradeQueryRequests", function() launch = orig_launch end) + -- Pass: Does not crash on 401, and passes error message + -- Fail: Crash, or returned error is wrong + it("does not crash on 401", function() + local json = '"{"error":"invalid_token","error_description":"The access token provided is invalid or has expired"}"' + local header = [[HTTP/1.1 401 Unauthorized +Date: Fri, 24 Apr 2026 07:30:38 GMT +Content-Type: application/json +Transfer-Encoding: chunked +Connection: keep-alive +Server: cloudflare +WWW-Authenticate: Bearer realm="pathofexile:production", error="invalid_token", error_description="The access token provided is invalid or has expired" +Cache-Control: no-store +Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] + local orig_launch = launch + launch = { + DownloadPage = function(url, onComplete, opts) + onComplete({ body = json, header = header }, nil) + end + } + table.insert(requests.requestQueue.search, { + url = "test", + callback = function(body, msg) + assert.are.equal(body, json) + assert.are.equal(msg, "Response code: 401\nAuthorization is invalid. Please Re-Log and reset") + end, + retryTime = nil + }) + local function mock_next_time(self, policy, time) + return time - 1 + end + mock_limiter.NextRequestTime = mock_next_time + requests:ProcessQueue() + assert.are.equal(#requests.requestQueue.search, 0) + launch = orig_launch + end) + -- Pass: Retries with increasing backoff up to cap, preventing infinite loops -- Fail: No backoff or uncapped, indicating retry bug, risking API bans it("retries on 429 with exponential backoff", function() @@ -192,4 +228,4 @@ describe("TradeQueryRequests", function() requests.FetchResultBlock = orig_fetchBlock end) end) -end) \ No newline at end of file +end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 8d6a824f98..954ed14fe6 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -8,39 +8,305 @@ local t_insert = table.insert local t_remove = table.remove local b_rshift = bit.rshift local band = bit.band +local m_max = math.max +local dkjson = require "dkjson" + -local realmList = { - { label = "PC", id = "PC", realmCode = "pc", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, - { label = "Xbox", id = "XBOX", realmCode = "xbox", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, - { label = "PS4", id = "SONY", realmCode = "sony", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, - { label = "Hotcool", id = "PC", realmCode = "pc", hostName = "https://pathofexile.tw/", profileURL = "account/view-profile/" }, - { label = "Tencent", id = "PC", realmCode = "pc", hostName = "https://poe.game.qq.com/", profileURL = "account/view-profile/" }, -} local influenceInfo = itemLib.influenceInfo.all -local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(self, build) - self.ControlHost() - self.Control() +function addOAuthControls(self) + self.usingOauth = true + self.isAuthorized = function() return main.api.authToken ~= nil end + -- the 30 second timer for oauth + --- @type integer? + self.oauthTimer = nil + -- timestamp for when we can request again after being rate limited + --- @type integer? + self.rateLimitEndTime = nil + --- @type string? + self.oauthErrCode = nil + --- @type bool + self.oauthLoading = false + -- an array of Character for each realm. Note this is for the character + -- list, which will mean that equipment and passives are missing + -- https://www.pathofexile.com/developer/docs/reference#type-Character + --- @type table + self.characterList = {} + + function charImportStatus() + if not self.isAuthorized() and not self.oauthTimer then + return colorCodes.WARNING .. "Not authenticated" .. (self.oauthErrCode or "") + elseif not self.isAuthorized() and self.oauthTimer then + local timeLeft = m_max(0, (self.oauthTimer + 30) - os.time()) + if timeLeft < 1 then + self.oauthTimer = nil + return colorCodes.WARNING .. "Not authenticated" .. (self.oauthErrCode or "") + end + return string.format("Logging in... (%d)", timeLeft) .. (self.oauthErrCode or "") + -- user is spam changing realms and is rate limited + elseif self.isAuthorized() and self.rateLimitEndTime then + local timeLeft = m_max(0, self.rateLimitEndTime - os.time()) + if timeLeft < 0.5 then + self.rateLimitEndTime = nil + return "Authenticated" + end + return colorCodes.WARNING .. string.format("You're doing that too fast. Please wait (%d)", timeLeft) + elseif self.isAuthorized() and self.oauthLoading then + return "Fetching..." + elseif self.isAuthorized() then + return "Authenticated" + end + -- unreachable + return "" + end - self.build = build + -- space after labels + local labelSpacing = 6 + -- space between rows + local rowSpacing = 6 + + + self.controls.charImportStatusLabel = new("LabelControl", { "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, + { labelSpacing, 14, 200, 16 }, function() + return "^7Character import status: " .. charImportStatus() + end) + + self.controls.logoutApiButton = new("ButtonControl", { "TOPLEFT", self.controls.charImportStatusLabel, "TOPRIGHT" }, + { labelSpacing, 0, 170, 16 }, "^7Logout from Path of Exile API", function() + main.api:ResetDetails() + main:SaveSettings() + end) + self.controls.logoutApiButton.shown = function() return self.usingOauth and self.isAuthorized() end + + self.controls.characterImportAnchor = new("Control", { "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, + { labelSpacing, 40, 200, 16 }) + self.controls.sectionOauthCharImport.height = function() + return self.isAuthorized() and 200 or 60 + end + + -- OAuth Stage: Authenticate + self.controls.authenticateButton = new("ButtonControl", { "TOPLEFT", self.controls.characterImportAnchor, "TOPLEFT" }, + { 0, 0, 200, 16 }, "^7Authorize with Path of Exile", function() + main.api:FetchAuthToken(function(errCode) + if errCode then + self.oauthErrCode = errCode + self.oauthTimer = nil + else + self.oauthErrCode = nil + ConPrintf("%s", main.api.authToken) + self.oauthTimer = nil + end + end) + self.oauthTimer = os.time() + end) + self.controls.authenticateButton.shown = function() + return self.usingOauth and not self.isAuthorized() + end + + -- Stage: select realm, league, character, and import data + self.controls.charSelectHeader = new("LabelControl", { "TOPLEFT", self.controls.sectionOauthCharImport, "TOPLEFT" }, + { labelSpacing, 40, 200, 16 }, "^7Choose character to import data from:") + self.controls.charSelectHeader.shown = function() + return self.usingOauth and self.isAuthorized() + end + + -- realm select + function setLeaguesFromCharList() + local currentRealm = self.controls.accountRealm:GetSelValue().realmCode + local currentCharacters = currentRealm and self.characterList[currentRealm] + if not currentCharacters or #currentCharacters == 0 then + self.controls.charSelectLeague:SetList({}) + self.controls.charSelect:SetList({}) + return + end + local set = {} + + for _, character in ipairs(currentCharacters) do + -- the api reference says league is (somehow) not necessarily present + if character.league then + set[character.league] = true + end + end + local ret = {} + + for key, _ in pairs(set) do + t_insert(ret, key) + end + table.sort(ret, function(a, b) + return a:lower() < b:lower() + end) + table.insert(ret, "Any") + + self.controls.charSelectLeague:SetList(ret) + self.controls.charSelectLeague.selIndex = nil + if main.lastLeague then + for i, v in ipairs(self.controls.charSelectLeague.list) do + if v == main.lastLeague then + self.controls.charSelectLeague:SetSel(i) + end + end + else + self.controls.charSelectLeague:SetSel(1) + end + end + function fetchCharacters() + local realm = self.controls.accountRealm:GetSelValue() + self.oauthLoading = true + function onResponse(body, err, timeNext) + if not err then + self.characterList[realm.realmCode] = body.characters + setLeaguesFromCharList() + self.oauthLoading = false + return + elseif err == "Response code: 429" then + self.rateLimitEndTime = timeNext + else + self.oauthErrCode = err + end + self.oauthLoading = false + end + main.api:DownloadCharacterList(realm.realmCode, onResponse) + end + + local realmList = { + { label = "PC", id = "PC", realmCode = "pc"}, + { label = "Xbox", id = "XBOX", realmCode = "xbox"}, + { label = "Sony", id = "SONY", realmCode = "sony" }, + } + self.controls.accountRealm = new("DropDownControl", { "TOPLEFT", self.controls.charSelectHeader, "BOTTOMLEFT" }, + { 0, rowSpacing, 60, 20 }, realmList, function() + setLeaguesFromCharList() + end) + self.controls.accountRealm:SelByValue(main.lastRealm or "PC", "id") + function fetchTextFunc() + local realm = self.controls.accountRealm:GetSelValue() + if realm and self.characterList[realm.realmCode] then + return "Fetched" + end + return "Fetch Characters" + end + function fetchButtonEnabled() + local realm = self.controls.accountRealm:GetSelValue() + return not (realm and self.characterList[realm.realmCode]) + end + self.controls.accountRealmFetchButton = new("ButtonControl", { "LEFT", self.controls.accountRealm, "RIGHT" }, + { labelSpacing, 0, 130, 20 }, fetchTextFunc, fetchCharacters) + self.controls.accountRealmFetchButton.enabled = fetchButtonEnabled + + -- league select + --- @param newLeague string + function onLeagueChange(_, newLeague) + local realm = self.controls.accountRealm:GetSelValue().realmCode + if newLeague == "Any" then + self:BuildCharacterList(realm, nil, self.characterList[realm], self.controls.charSelect) + else + self:BuildCharacterList(realm, newLeague, self.characterList[realm], self.controls.charSelect) + end + end + + self.controls.charSelectLeagueLabel = new("LabelControl", { "TOPLEFT", self.controls.accountRealm, "BOTTOMLEFT" }, + { 0, rowSpacing, 0, 14 }, "^7League:") + self.controls.charSelectLeague = new("DropDownControl", { "LEFT", self.controls.charSelectLeagueLabel, "RIGHT" }, + { labelSpacing, 0, 150, 18 }, nil, onLeagueChange) + -- character select + self.controls.charSelect = new("DropDownControl", { "TOPLEFT", self.controls.charSelectLeagueLabel, "BOTTOMLEFT" }, + { 0, rowSpacing, 400, 18 }, nil) + self.controls.charSelect.enabled = function() + return self.usingOauth and self.isAuthorized() + end + + -- import action controls + local function saveDetails(realmId, league, charName) + main.lastRealm = realmId + self.lastRealm = realmId + main.lastLeague = league + self.lastLeague = league + main.lastCharacterHash = common.sha1(charName) + self.lastCharacterHash = common.sha1(charName) + end + self.controls.charImportHeader = new("LabelControl", { "TOPLEFT", self.controls.charSelect, "BOTTOMLEFT" }, + { 0, rowSpacing, 200, 16 }, "^7Import:") + self.controls.charImportTree = new("ButtonControl", { "LEFT", self.controls.charImportHeader, "RIGHT" }, + { labelSpacing, 0, 170, 20 }, "Passive Tree and Jewels", function() + local realm = self.controls.accountRealm:GetSelValue() + local league = self.controls.charSelectLeague:GetSelValue() + local selectedName = self.controls.charSelect:GetSelValue().label + + saveDetails(realm.id, league, selectedName) + local deleteJewels = self.controls.charImportTreeClearJewels.state + if self.build.spec:CountAllocNodes() > 0 then + main:OpenConfirmPopup("Character Import", "Importing the passive tree will overwrite your current tree.", + "Import", function() + main.api:DownloadCharacter(realm.realmCode, selectedName, function(char) + self:ImportPassiveTreeAndJewels(char.character, deleteJewels) + end) + end) + else + main.api:DownloadCharacter(realm.realmCode, selectedName, function(char) + self:ImportPassiveTreeAndJewels(char.character, deleteJewels) + end) + end + end) + self.controls.charImportTree.enabled = function() + return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() + end + self.controls.charImportTreeClearJewels = new("CheckBoxControl", { "LEFT", self.controls.charImportTree, "RIGHT" }, + { 90, 0, 18 }, "Delete jewels:", nil, "Delete all equipped jewels when importing.", true) + self.controls.charImportItems = new("ButtonControl", { "TOPLEFT", self.controls.charImportTree, "BOTTOMLEFT" }, + { 0, rowSpacing, 110, 20 }, "Items and Skills", function() + local realm = self.controls.accountRealm:GetSelValue() + local league = self.controls.charSelectLeague:GetSelValue() + local selectedName = self.controls.charSelect:GetSelValue().label + + saveDetails(realm.id, league, selectedName) + + main.api:DownloadCharacter(realm.realmCode, selectedName, function(char) + local clearItems = self.controls.charImportItemsClearItems.state + local clearSkills = self.controls.charImportItemsClearSkills.state + local ignoreWeaponSwap = self.controls.charImportItemsIgnoreWeaponSwap.state + self:ImportItemsAndSkills(char.character, clearItems, clearSkills, ignoreWeaponSwap) + end) + end) + self.controls.charImportItems.enabled = function() + return self.usingOauth and self.isAuthorized() and self.controls.charSelect:GetSelValue() + end + self.controls.charImportItemsClearSkills = new("CheckBoxControl", { "LEFT", self.controls.charImportItems, "RIGHT" }, + { 85, 0, 18 }, "Delete skills:", nil, "Delete all existing skills when importing.", true) + self.controls.charImportItemsClearItems = new("CheckBoxControl", { "LEFT", self.controls.charImportItems, "RIGHT" }, + { 220, 0, 18 }, "Delete equipment:", nil, "Delete all equipped items when importing.", true) + self.controls.charImportItemsIgnoreWeaponSwap = new("CheckBoxControl", { "LEFT", self.controls.charImportItems, + "RIGHT" }, { 380, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) +end +function addAccountNameControls(self) self.charImportMode = "GETACCOUNTNAME" self.charImportStatus = "Idle" - self.controls.sectionCharImport = new("SectionControl", {"TOPLEFT",self,"TOPLEFT"}, {10, 18, 650, 250}, "Character Import") - self.controls.charImportStatusLabel = new("LabelControl", {"TOPLEFT",self.controls.sectionCharImport,"TOPLEFT"}, {6, 14, 200, 16}, function() - return "^7Character import status: "..self.charImportStatus + self.controls.siteCharImportStatusLabel = new("LabelControl", { "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, + { 6, 14, 200, 16 }, function() + return "^7Character import status: " .. self.charImportStatus end) -- Stage: input account name - self.controls.accountNameHeader = new("LabelControl", {"TOPLEFT",self.controls.sectionCharImport,"TOPLEFT"}, {6, 40, 200, 16}, "^7To start importing a character, enter the character's account name:") - self.controls.accountNameHeader.shown = function() + self.controls.siteAccountNameHeader = new("LabelControl", { "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, + { 6, 40, 250, 16 }, "^7To start importing a character, enter the character's account name:") + self.controls.siteAccountNameHeader.shown = function() return self.charImportMode == "GETACCOUNTNAME" end - self.controls.accountRealm = new("DropDownControl", {"TOPLEFT",self.controls.accountNameHeader,"BOTTOMLEFT"}, {0, 4, 60, 20}, realmList) - self.controls.accountRealm:SelByValue(main.lastRealm or "PC", "id") - self.controls.accountName = new("EditControl", {"LEFT",self.controls.accountRealm,"RIGHT"}, {8, 0, 200, 20}, main.lastAccountName or "", nil, "%c", nil, nil, nil, nil, true) - self.controls.accountName.pasteFilter = function(text) + local realmList = { + { label = "PC", id = "PC", realmCode = "pc", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, + { label = "Xbox", id = "XBOX", realmCode = "xbox", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, + { label = "Sony", id = "SONY", realmCode = "sony", hostName = "https://www.pathofexile.com/", profileURL = "account/view-profile/" }, + { label = "Hotcool", id = "PC", realmCode = "pc", hostName = "https://pathofexile.tw/", profileURL = "account/view-profile/" }, + { label = "Tencent", id = "PC", realmCode = "pc", hostName = "https://poe.game.qq.com/", profileURL = "account/view-profile/" }, + } + self.controls.siteAccountRealm = new("DropDownControl", + { "TOPLEFT", self.controls.siteAccountNameHeader, "BOTTOMLEFT" }, + { 0, 4, 60, 20 }, realmList) + self.controls.siteAccountRealm:SelByValue(main.lastRealm or "PC", "id") + self.controls.siteAccountName = new("EditControl", { "LEFT", self.controls.siteAccountRealm, "RIGHT" }, { 8, 0, 200, 20 }, + main.lastAccountName or "", nil, "%c", nil, nil, nil, nil, true) + self.controls.siteAccountName.pasteFilter = function(text) return text:gsub(".", function(c) local byte = c:byte() if byte >= 128 then @@ -52,140 +318,154 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function( end -- accountHistory Control if not historyList then - historyList = { } + historyList = {} for accountName, account in pairs(main.gameAccounts) do t_insert(historyList, accountName) historyList[accountName] = true end - table.sort(historyList, function(a,b) + table.sort(historyList, function(a, b) return a:lower() < b:lower() end) end -- don't load the list many times - self.controls.accountNameGo = new("ButtonControl", {"LEFT",self.controls.accountName,"RIGHT"}, {8, 0, 60, 20}, "Start", function() - self.controls.sessionInput.buf = "" - self:DownloadCharacterList() - end) - self.controls.accountNameGo.enabled = function() - return self.controls.accountName.buf:match("%S[#%-]%d%d%d%d$") + self.controls.siteAccountNameGo = new("ButtonControl", { "LEFT", self.controls.siteAccountName, "RIGHT" }, { 8, 0, 60, 20 }, + "Start", function() + local realm = self.controls.siteAccountRealm:GetSelValue() + self:DownloadSiteCharacterList(realm) + end) + self.controls.siteAccountNameGo.enabled = function() + return self.controls.siteAccountName.buf:match("%S[#%-]%d%d%d%d$") end - self.controls.accountNameGo.tooltipFunc = function(tooltip) + self.controls.siteAccountNameGo.tooltipFunc = function(tooltip) tooltip:Clear() - if not self.controls.accountName.buf:match("[#%-]%d%d%d%d$") and self.controls.accountName.buf ~= "" then - tooltip:AddLine(16, "^7Missing discriminator e.g. " .. self.controls.accountName.buf .. "#1234") + if not self.controls.siteAccountName.buf:match("[#%-]%d%d%d%d$") and self.controls.siteAccountName.buf ~= "" then + tooltip:AddLine(16, "^7Missing discriminator e.g. " .. self.controls.siteAccountName.buf .. "#1234") end end - self.controls.accountHistory = new("DropDownControl", {"LEFT",self.controls.accountNameGo,"RIGHT"}, {8, 0, 200, 20}, historyList, function() - self.controls.accountName.buf = self.controls.accountHistory.list[self.controls.accountHistory.selIndex] + self.controls.siteAccountHistory = new("DropDownControl", { "LEFT", self.controls.siteAccountNameGo, "RIGHT" }, + { 8, 0, 200, 20 }, historyList, function() + self.controls.siteAccountName.buf = self.controls.siteAccountHistory.list[self.controls.siteAccountHistory.selIndex] end) - self.controls.accountHistory:SelByValue(main.lastAccountName) - self.controls.accountHistory:CheckDroppedWidth(true) + self.controls.siteAccountHistory:SelByValue(main.lastAccountName) + self.controls.siteAccountHistory:CheckDroppedWidth(true) - self.controls.removeAccount = new("ButtonControl", {"LEFT",self.controls.accountHistory,"RIGHT"}, {8, 0, 20, 20}, "X", function() - local accountName = self.controls.accountHistory.list[self.controls.accountHistory.selIndex] + self.controls.siteRemoveAccount = new("ButtonControl", { "LEFT", self.controls.siteAccountHistory, "RIGHT" }, { 8, 0, 20, 20 }, + "X", function() + local accountName = self.controls.siteAccountHistory.list[self.controls.siteAccountHistory.selIndex] if (accountName ~= nil) then - t_remove(self.controls.accountHistory.list, self.controls.accountHistory.selIndex) - self.controls.accountHistory.list[accountName] = nil - main.gameAccounts[accountName] = nil + t_remove(self.controls.siteAccountHistory.list, self.controls.siteAccountHistory.selIndex) + self.controls.siteAccountHistory.list[accountName] = nil + main.gameAccounts[accountName] = nil end end) - self.controls.removeAccount.tooltipFunc = function(tooltip) + self.controls.siteRemoveAccount.tooltipFunc = function(tooltip) tooltip:Clear() tooltip:AddLine(16, "^7Removes account from the dropdown list") end - self.controls.accountNameMissingDiscriminator = new("LabelControl", {"TOPLEFT",self.controls.accountName,"BOTTOMLEFT"}, {0, 8, 0, 16}, "^1Missing discriminator e.g. #1234") - self.controls.accountNameMissingDiscriminator.shown = function() - return not self.controls.accountName.buf:match("[#%-]%d%d%d%d$") and self.controls.accountName.buf ~= "" + self.controls.siteAccountNameMissingDiscriminator = new("LabelControl", + { "TOPLEFT", self.controls.siteAccountName, "BOTTOMLEFT" }, { 0, 8, 0, 16 }, "^1Missing discriminator e.g. #1234") + self.controls.siteAccountNameMissingDiscriminator.shown = function() + return not self.controls.siteAccountName.buf:match("[#%-]%d%d%d%d$") and self.controls.siteAccountName.buf ~= "" end - self.controls.accountNameUnicode = new("LabelControl", {"TOPLEFT",self.controls.accountRealm,"BOTTOMLEFT"}, {0, 34, 0, 14}, "^7Note: if the account name contains non-ASCII characters it must be pasted into the textbox,\nnot typed manually.") - - -- Stage: input POESESSID - self.controls.sessionHeader = new("LabelControl", {"TOPLEFT",self.controls.sectionCharImport,"TOPLEFT"}, {6, 40, 200, 14}) - self.controls.sessionHeader.label = function() - return [[ -^7The list of characters on ']]..self.controls.accountName.buf..[[' couldn't be retrieved. This may be because: -1. You are missing the discriminator at the end of the account name e.g. #1234 -2. You entered a character name instead of an account name or -3. This account's characters tab is hidden (this is the default setting). -If this is your account, you can either: -1. Uncheck "Hide Characters" in your privacy settings or -2. Enter a POESESSID below. -You can get this from your web browser's cookies while logged into the Path of Exile website. - ]] - end - self.controls.sessionHeader.shown = function() - return self.charImportMode == "GETSESSIONID" - end - self.controls.sessionRetry = new("ButtonControl", {"TOPLEFT",self.controls.sessionHeader,"TOPLEFT"}, {0, 122, 60, 20}, "Retry", function() - self:DownloadCharacterList() - end) - self.controls.sessionCancel = new("ButtonControl", {"LEFT",self.controls.sessionRetry,"RIGHT"}, {8, 0, 60, 20}, "Cancel", function() - self.charImportMode = "GETACCOUNTNAME" - self.charImportStatus = "Idle" - end) - self.controls.sessionPrivacySettings = new("ButtonControl", {"LEFT",self.controls.sessionCancel,"RIGHT"}, {8, 0, 120, 20}, "Privacy Settings", function() - OpenURL('https://www.pathofexile.com/my-account/privacy') - end) - self.controls.sessionInput = new("EditControl", {"TOPLEFT",self.controls.sessionRetry,"BOTTOMLEFT"}, {0, 8, 350, 20}, "", "POESESSID", "%X", 32) - self.controls.sessionInput:SetProtected(true) - self.controls.sessionGo = new("ButtonControl", {"LEFT",self.controls.sessionInput,"RIGHT"}, {8, 0, 60, 20}, "Go", function() - self:DownloadCharacterList() - end) - self.controls.sessionGo.enabled = function() - return #self.controls.sessionInput.buf == 32 - end + self.controls.siteAccountNameUnicode = new("LabelControl", { "TOPLEFT", self.controls.siteAccountRealm, "BOTTOMLEFT" }, + { 0, 34, 0, 14 }, + "^7Note: if the account name contains non-ASCII characters it must be pasted into the textbox,\nnot typed manually.") -- Stage: select character and import data - self.controls.charSelectHeader = new("LabelControl", {"TOPLEFT",self.controls.sectionCharImport,"TOPLEFT"}, {6, 40, 200, 16}, "^7Choose character to import data from:") - self.controls.charSelectHeader.shown = function() + self.controls.siteCharSelectHeader = new("LabelControl", { "TOPLEFT", self.controls.sectionCharSiteImport, "TOPLEFT" }, + { 6, 40, 200, 16 }, "^7Choose character to import data from:") + self.controls.siteCharSelectHeader.shown = function() return self.charImportMode == "SELECTCHAR" or self.charImportMode == "IMPORTING" end - self.controls.charSelectLeagueLabel = new("LabelControl", {"TOPLEFT",self.controls.charSelectHeader,"BOTTOMLEFT"}, {0, 6, 0, 14}, "^7League:") - self.controls.charSelectLeague = new("DropDownControl", {"LEFT",self.controls.charSelectLeagueLabel,"RIGHT"}, {4, 0, 150, 18}, nil, function(index, value) - self:BuildCharacterList(value.league) - end) - self.controls.charSelect = new("DropDownControl", {"TOPLEFT",self.controls.charSelectHeader,"BOTTOMLEFT"}, {0, 24, 400, 18}) - self.controls.charSelect.enabled = function() + self.controls.siteCharSelectLeagueLabel = new("LabelControl", { "TOPLEFT", self.controls.siteCharSelectHeader, "BOTTOMLEFT" }, + { 0, 6, 0, 14 }, "^7League:") + self.controls.siteCharSelectLeague = new("DropDownControl", { "LEFT", self.controls.siteCharSelectLeagueLabel, "RIGHT" }, + { 4, 0, 150, 18 }, nil, function(index, value) + self:BuildCharacterList("pc", value.league, self.lastCharList, self.controls.siteCharSelect) + end) + self.controls.siteCharSelect = new("DropDownControl", { "TOPLEFT", self.controls.siteCharSelectHeader, "BOTTOMLEFT" }, + { 0, 24, 400, 18 }) + self.controls.siteCharSelect.enabled = function() return self.charImportMode == "SELECTCHAR" end - self.controls.charImportHeader = new("LabelControl", {"TOPLEFT",self.controls.charSelect,"BOTTOMLEFT"}, {0, 16, 200, 16}, "^7Import:") - self.controls.charImportTree = new("ButtonControl", {"LEFT",self.controls.charImportHeader, "RIGHT"}, {8, 0, 170, 20}, "Passive Tree and Jewels", function() - if self.build.spec:CountAllocNodes() > 0 then - main:OpenConfirmPopup("Character Import", "Importing the passive tree will overwrite your current tree.", "Import", function() - self:DownloadPassiveTree() - end) - else - self:DownloadPassiveTree() - end - self:SetPredefinedBuildName() - end) - self.controls.charImportTree.enabled = function() + self.controls.siteCharImportHeader = new("LabelControl", { "TOPLEFT", self.controls.siteCharSelect, "BOTTOMLEFT" }, + { 0, 16, 200, 16 }, "^7Import:") + self.controls.siteCharImportTree = new("ButtonControl", { "LEFT", self.controls.siteCharImportHeader, "RIGHT" }, + { 8, 0, 170, 20 }, "Passive Tree and Jewels", function() + local realm = self.controls.siteAccountRealm:GetSelValue() + if self.build.spec:CountAllocNodes() > 0 then + main:OpenConfirmPopup("Character Import", "Importing the passive tree will overwrite your current tree.", + "Import", function() + self:DownloadPassiveTree(realm) + end) + else + self:DownloadPassiveTree(realm) + end + self:SetPredefinedBuildName() + end) + self.controls.siteCharImportTree.enabled = function() return self.charImportMode == "SELECTCHAR" end - self.controls.charImportTreeClearJewels = new("CheckBoxControl", {"LEFT",self.controls.charImportTree,"RIGHT"}, {90, 0, 18}, "Delete jewels:", nil, "Delete all existing jewels when importing.", true) - self.controls.charImportItems = new("ButtonControl", {"LEFT",self.controls.charImportTree, "LEFT"}, {0, 36, 110, 20}, "Items and Skills", function() - self:DownloadItems() - self:SetPredefinedBuildName() - end) - self.controls.charImportItems.enabled = function() + self.controls.siteCharImportTreeClearJewels = new("CheckBoxControl", { "LEFT", self.controls.siteCharImportTree, "RIGHT" }, + { 90, 0, 18 }, "Delete jewels:", nil, "Delete all equipped jewels when importing.", true) + self.controls.siteCharImportItems = new("ButtonControl", { "LEFT", self.controls.siteCharImportTree, "LEFT" }, + { 0, 36, 110, 20 }, "Items and Skills", function() + local realm = self.controls.siteAccountRealm:GetSelValue() + self:DownloadItems(realm) + self:SetPredefinedBuildName() + end) + self.controls.siteCharImportItems.enabled = function() return self.charImportMode == "SELECTCHAR" end - self.controls.charImportItemsClearSkills = new("CheckBoxControl", {"LEFT",self.controls.charImportItems,"RIGHT"}, {85, 0, 18}, "Delete skills:", nil, "Delete all existing skills when importing.", true) - self.controls.charImportItemsClearItems = new("CheckBoxControl", {"LEFT",self.controls.charImportItems,"RIGHT"}, {220, 0, 18}, "Delete equipment:", nil, "Delete all equipped items when importing.", true) - self.controls.charImportItemsIgnoreWeaponSwap = new("CheckBoxControl", {"LEFT",self.controls.charImportItems,"RIGHT"}, {380, 0, 18}, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) - self.controls.charBanditNote = new("LabelControl", {"TOPLEFT",self.controls.charImportHeader,"BOTTOMLEFT"}, {0, 50, 200, 14}, "^7Tip: After you finish importing a character, make sure you update the bandit choice,\nas it cannot be imported.") - - self.controls.charClose = new("ButtonControl", {"TOPLEFT",self.controls.charImportHeader,"BOTTOMLEFT"}, {0, 90, 60, 20}, "Close", function() + self.controls.siteCharImportItemsClearSkills = new("CheckBoxControl", { "LEFT", self.controls.siteCharImportItems, "RIGHT" }, + { 85, 0, 18 }, "Delete skills:", nil, "Delete all existing skills when importing.", true) + self.controls.siteCharImportItemsClearItems = new("CheckBoxControl", { "LEFT", self.controls.siteCharImportItems, "RIGHT" }, + { 220, 0, 18 }, "Delete equipment:", nil, "Delete all equipped items when importing.", true) + self.controls.siteCharImportItemsIgnoreWeaponSwap = new("CheckBoxControl", { "LEFT", self.controls.siteCharImportItems, + "RIGHT" }, { 380, 0, 18 }, "Ignore weapon swap:", nil, "Ignore items and skills in weapon swap.", false) + self.controls.siteCharBanditNote = new("LabelControl", { "TOPLEFT", self.controls.siteCharImportHeader, "BOTTOMLEFT" }, + { 0, 50, 200, 14 }, + "^7Tip: After you finish importing a character, make sure you update the bandit choice,\nas it can only be imported by logging in above.") + + self.controls.siteCharClose = new("ButtonControl", { "TOPLEFT", self.controls.siteCharImportHeader, "BOTTOMLEFT" }, + { 0, 90, 60, 20 }, "Close", function() self.charImportMode = "GETACCOUNTNAME" self.charImportStatus = "Idle" end) +end + +local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(self, build) + self.ControlHost() + self.Control() + + self.build = build + + if not main.api then + main.api = new("PoEAPI", main.lastToken, main.lastRefreshToken, main.tokenExpiry) + end + + + self.controls.sectionOauthCharImport = new("SectionControl", { "TOPLEFT", self, "TOPLEFT" }, { 10, 18, 650, 200 }, + "Import From Your Account") + + addOAuthControls(self) + + self.controls.sectionCharSiteImport = new("SectionControl", + { "TOPLEFT", self.controls.sectionOauthCharImport, "BOTTOMLEFT" }, + { 0, 18, 650, 250 }, + "Import By Account Name") + addAccountNameControls(self) + -- Build import/export - self.controls.sectionBuild = new("SectionControl", {"TOPLEFT",self.controls.sectionCharImport,"BOTTOMLEFT"}, {0, 18, 650, 182}, "Build Sharing") - self.controls.generateCodeLabel = new("LabelControl", {"TOPLEFT",self.controls.sectionBuild,"TOPLEFT"}, {6, 14, 0, 16}, "^7Generate a code to share this build with other Path of Building users:") + self.controls.sectionBuild = new("SectionControl", + { "TOPLEFT", self.controls.sectionCharSiteImport, "BOTTOMLEFT", true }, + { 0, 18, 650, 182 }, "Build Sharing") + self.controls.generateCodeLabel = new("LabelControl", { "TOPLEFT", self.controls.sectionBuild, "TOPLEFT" }, + { 6, 14, 0, 16 }, "^7Generate a code to share this build with other Path of Building users:") self.controls.generateCode = new("ButtonControl", {"LEFT",self.controls.generateCodeLabel,"RIGHT"}, {4, 0, 80, 20}, "Generate", function() self.controls.generateCodeOut:SetText(common.base64.encode(Deflate(self.build:SaveDB("code"))):gsub("+","-"):gsub("/","_")) end) @@ -218,10 +498,10 @@ You can get this from your web browser's cookies while logged into the Path of E local exportWebsitesList = getExportSitesFromImportList() self.controls.exportFrom = new("DropDownControl", { "LEFT", self.controls.generateCodeCopy,"RIGHT"}, {8, 0, 120, 20}, exportWebsitesList, function(_, selectedWebsite) - main.lastExportedWebsite = selectedWebsite.id + main.lastExportWebsite = selectedWebsite.id self.exportWebsiteSelected = selectedWebsite.id end) - self.controls.exportFrom:SelByValue(self.exportWebsiteSelected or main.lastExportedWebsite or "Maxroll", "id") + self.controls.exportFrom:SelByValue(self.exportWebsiteSelected or main.lastExportWebsite or "Pastebin", "id") self.controls.generateCodeByLink = new("ButtonControl", { "LEFT", self.controls.exportFrom, "RIGHT"}, {8, 0, 100, 20}, "Share", function() local exportWebsite = exportWebsitesList[self.controls.exportFrom.selIndex] local subScriptId = buildSites.UploadBuild(self.controls.generateCodeOut.buf, exportWebsite) @@ -262,6 +542,7 @@ You can get this from your web browser's cookies while logged into the Path of E self.importCodeDetail = "" self.importCodeXML = nil self.importCodeValid = false + self.importCodeJson = nil if #buf == 0 then return @@ -291,6 +572,28 @@ You can get this from your web browser's cookies while logged into the Path of E end end + -- If we are in dev mode and the string is a json + if launch.devMode and urlText:match("^%{.*%}$") ~= nil then + local jsonData, _, errDecode = dkjson.decode(urlText) + if errDecode then + self.importCodeDetail = colorCodes.NEGATIVE.."Invalid JSON format (decode error)" + return + end + if not jsonData.character then + self.importCodeDetail = colorCodes.NEGATIVE.."Invalid JSON format (character missing)" + return + end + jsonData = jsonData.character + if not jsonData.equipment or not jsonData.passives then + self.importCodeDetail = colorCodes.NEGATIVE.."Invalid JSON format (equipment or passives missing)" + return + end + self.importCodeJson = jsonData + self.importCodeDetail = colorCodes.POSITIVE.."JSON is valid" + self.importCodeValid = true + return + end + local xmlText = Inflate(common.base64.decode(buf:gsub("-","+"):gsub("_","/"))) if not xmlText then return @@ -361,6 +664,12 @@ You can get this from your web browser's cookies while logged into the Path of E return end + if self.importCodeJson then + self:ImportItemsAndSkills(self.importCodeJson, true, true, false) + self:ImportPassiveTreeAndJewels(self.importCodeJson, true) + return + end + importSelectedBuild() end) self.controls.importCodeGo.label = function () @@ -374,21 +683,22 @@ You can get this from your web browser's cookies while logged into the Path of E self.controls.importCodeGo.onClick() end end + + -- -- validate the status of the api the first time + main.api:ValidateAuth(function() end) end) function ImportTabClass:Load(xml, fileName) self.lastRealm = xml.attrib.lastRealm - self.controls.accountRealm:SelByValue(self.lastRealm or main.lastRealm or "PC", "id") self.lastLeague = xml.attrib.lastLeague - self.controls.charSelectLeague:SelByValue(self.lastLeague or "Standard", "id") self.lastAccountHash = xml.attrib.lastAccountHash self.importLink = xml.attrib.importLink self.controls.enablePartyExportBuffs.state = xml.attrib.exportParty == "true" self.build.partyTab.enableExportBuffs = self.controls.enablePartyExportBuffs.state - if self.lastAccountHash then + if self.lastAccountHash and false then for accountName in pairs(main.gameAccounts) do if common.sha1(accountName) == self.lastAccountHash then - self.controls.accountName:SetText(accountName) + self.controls.siteAccountName:SetText(accountName) end end end @@ -425,7 +735,100 @@ function ImportTabClass:Draw(viewPort, inputEvents) self:DrawControls(viewPort) end -function ImportTabClass:DownloadCharacterList() +function ImportTabClass:ProcessSiteJSON(json) + local func, errMsg = loadstring("return " .. jsonToLua(json)) + if errMsg then + return nil, errMsg + end + setfenv(func, {}) -- Sandbox the function just in case + local data = func() + if type(data) ~= "table" then + return nil, "Return type is not a table" + end + return data +end + +function ImportTabClass:SaveAccountHistory() + if not historyList[self.controls.siteAccountName.buf] then + t_insert(historyList, self.controls.siteAccountName.buf) + historyList[self.controls.siteAccountName.buf] = true + table.sort(historyList, function(a, b) + return a:lower() < b:lower() + end) + self.controls.accountHistory:CheckDroppedWidth(true) + self.controls.accountHistory:SelByValue(self.controls.siteAccountName.buf) + end +end + +function ImportTabClass:DownloadPassiveTree(realm) + self.charImportMode = "IMPORTING" + self.charImportStatus = "Retrieving character passive tree..." + local accountName = self.controls.siteAccountName.buf + local charSelect = self.controls.siteCharSelect + local charListData = charSelect.list[charSelect.selIndex].char + launch:DownloadPage( + realm.hostName .. + "character-window/get-passive-skills?accountName=" .. + accountName:gsub("#", "%%23") .. "&character=" .. urlEncode(charListData.name) .. "&realm=" .. realm.realmCode, + function(response, errMsg) + self.charImportMode = "SELECTCHAR" + if errMsg then + self.charImportStatus = colorCodes.NEGATIVE .. + "Error importing character data, try again (" .. errMsg:gsub("\n", " ") .. ")" + return + elseif response.body == "false" then + self.charImportStatus = colorCodes.NEGATIVE .. "Failed to retrieve character data, try again." + return + end + self.lastCharacterHash = common.sha1(charListData.name) + if not self.lastLeague then + self.lastLeague = charSelectLeague:GetSelValueByKey("league") + end + local responseLua = dkjson.decode(response.body) + -- modify response to be like the oauth API response + local charData = copyTable(charListData) + charData.passives = responseLua + charData.jewels = responseLua.items + local deleteJewels = self.controls.siteCharImportTreeClearJewels.state + self:ImportPassiveTreeAndJewels(charData, deleteJewels) + end) +end + +function ImportTabClass:DownloadItems(realm) + self.charImportMode = "IMPORTING" + self.charImportStatus = "Retrieving character items..." + local accountName = self.controls.siteAccountName.buf + local charSelect = self.controls.siteCharSelect + local charListData = charSelect.list[charSelect.selIndex].char + launch:DownloadPage( + realm.hostName .. + "character-window/get-items?accountName=" .. + accountName:gsub("#", "%%23") .. "&character=" .. urlEncode(charListData.name) .. "&realm=" .. realm.realmCode, + function(response, errMsg) + self.charImportMode = "SELECTCHAR" + if errMsg then + self.charImportStatus = colorCodes.NEGATIVE .. + "Error importing character data, try again (" .. errMsg:gsub("\n", " ") .. ")" + return + elseif response.body == "false" then + self.charImportStatus = colorCodes.NEGATIVE .. "Failed to retrieve character data, try again." + return + end + self.lastCharacterHash = common.sha1(charListData.name) + if not self.lastLeague then + self.lastLeague = charSelectLeague:GetSelValueByKey("league") + end + local responseLua = dkjson.decode(response.body) + -- modify response to be like the oauth API response + local charData = copyTable(charListData) + charData.equipment = responseLua.items + local clearItems = self.controls.siteCharImportItemsClearItems.state + local clearSkills = self.controls.siteCharImportItemsClearSkills.state + local ignoreWeaponSwap = self.controls.siteCharImportItemsIgnoreWeaponSwap.state + self:ImportItemsAndSkills(charData, clearItems, clearSkills, ignoreWeaponSwap) + end) +end +function ImportTabClass:DownloadSiteCharacterList(realm) function FindMatchingStandardLeague(league) -- Find a Standard league name for a given league name -- Reference https://api.pathofexile.com/league?realm=pc @@ -441,126 +844,134 @@ function ImportTabClass:DownloadCharacterList() -- normal league and ruthless league (Sanctum, Ruthless Sanctum) return "Standard" end - end - + end + self.charImportMode = "DOWNLOADCHARLIST" self.charImportStatus = "Retrieving character list..." - local realm = realmList[self.controls.accountRealm.selIndex] local accountName -- Handle spaces in the account name if realm.realmCode == "pc" then - accountName = self.controls.accountName.buf:gsub("%s+", "") + accountName = self.controls.siteAccountName.buf:gsub("%s+", "") else - accountName = self.controls.accountName.buf:gsub("^[%s?]+", ""):gsub("[%s?]+$", ""):gsub("%s", "+") + accountName = self.controls.siteAccountName.buf:gsub("^[%s?]+", ""):gsub("[%s?]+$", ""):gsub("%s", "+") end accountName = accountName:gsub("(.*)[#%-]", "%1#") - local sessionID = #self.controls.sessionInput.buf == 32 and self.controls.sessionInput.buf or (main.gameAccounts[accountName] and main.gameAccounts[accountName].sessionID) - launch:DownloadPage(realm.hostName.."character-window/get-characters?accountName="..accountName:gsub("#", "%%23").."&realm="..realm.realmCode, function(response, errMsg) - if errMsg == "Response code: 401" then - self.charImportStatus = colorCodes.NEGATIVE.."Sign-in is required." - self.charImportMode = "GETSESSIONID" - return - elseif errMsg == "Response code: 403" then - self.charImportStatus = colorCodes.NEGATIVE.."Account profile is private." - self.charImportMode = "GETSESSIONID" - return - elseif errMsg == "Response code: 404" then - self.charImportStatus = colorCodes.NEGATIVE.."Account name is incorrect." - self.charImportMode = "GETACCOUNTNAME" - return - elseif errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error retrieving character list, try again ("..errMsg:gsub("\n"," ")..")" - self.charImportMode = "GETACCOUNTNAME" - return - end - local charList, errMsg = self:ProcessJSON(response.body) - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error processing character list, try again later" - self.charImportMode = "GETACCOUNTNAME" - return - end - --ConPrintTable(charList) - if #charList == 0 then - self.charImportStatus = colorCodes.NEGATIVE.."The account has no characters to import." - self.charImportMode = "GETACCOUNTNAME" - return - end - -- GGG's character API has an issue where for /get-characters the account name is not case-sensitive, but for /get-passive-skills and /get-items it is. - -- This workaround grabs the profile page and extracts the correct account name from one of the URLs. - launch:DownloadPage(realm.hostName..realm.profileURL..accountName:gsub("#", "%%23"), function(response, errMsg) - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error retrieving character list, try again ("..errMsg:gsub("\n"," ")..")" + launch:DownloadPage( + realm.hostName .. + "character-window/get-characters?accountName=" .. accountName:gsub("#", "%%23") .. "&realm=" .. realm.realmCode, + function(response, errMsg) + if errMsg == "Response code: 401" or errMsg == "Response code: 403" then + self.charImportStatus = colorCodes.NEGATIVE .. "Account profile is private or does not exist." self.charImportMode = "GETACCOUNTNAME" return - end - local realAccountName = response.body:match("/view%-profile/([^/]+)/characters"):gsub(".", function(c) if c:byte(1) > 127 then return string.format("%%%2X",c:byte(1)) else return c end end) - if not realAccountName then - self.charImportStatus = colorCodes.NEGATIVE.."Failed to retrieve character list." - self.charImportMode = "GETSESSIONID" + elseif errMsg == "Response code: 404" then + self.charImportStatus = colorCodes.NEGATIVE .. "Account name is incorrect." + self.charImportMode = "GETACCOUNTNAME" + return + elseif errMsg then + self.charImportStatus = colorCodes.NEGATIVE .. + "Error retrieving character list, try again (" .. errMsg:gsub("\n", " ") .. ")" + self.charImportMode = "GETACCOUNTNAME" return end - realAccountName = realAccountName:gsub("(.*)[#%-]", "%1#") - accountName = realAccountName - self.controls.accountName:SetText(realAccountName) - self.charImportStatus = "Character list successfully retrieved." - self.charImportMode = "SELECTCHAR" - self.lastRealm = realm.id - main.lastRealm = realm.id - self.lastAccountHash = common.sha1(accountName) - main.lastAccountName = accountName - main.gameAccounts[accountName] = main.gameAccounts[accountName] or { } - main.gameAccounts[accountName].sessionID = sessionID - local leagueList = { } - for i, char in ipairs(charList) do - if not isValueInArray(leagueList, char.league) then - t_insert(leagueList, char.league) - end + local charList, errMsg = self:ProcessSiteJSON(response.body) + if errMsg then + self.charImportStatus = colorCodes.NEGATIVE .. "Error processing character list, try again later" + self.charImportMode = "GETACCOUNTNAME" + return end - table.sort(leagueList) - charSelectLeague = self.controls.charSelectLeague - wipeTable(self.controls.charSelectLeague.list) - for _, league in ipairs(leagueList) do - t_insert(self.controls.charSelectLeague.list, { - label = league, - league = league, - }) + --ConPrintTable(charList) + if #charList == 0 then + self.charImportStatus = colorCodes.NEGATIVE .. "The account has no characters to import." + self.charImportMode = "GETACCOUNTNAME" + return end - t_insert(self.controls.charSelectLeague.list, { - label = "All", - }) - -- set the league combo to the last used if possible, used for previously imported characters - if self.lastLeague then - charSelectLeague:SelByValue(self.lastLeague, "league") - -- check that it worked - if charSelectLeague:GetSelValueByKey("league") ~= self.lastLeague then - -- League maybe over, Character will be in standard - standardLeagueName = FindMatchingStandardLeague(self.lastLeague) - self.controls.charSelectLeague:SelByValue(standardLeagueName, "league") - if charSelectLeague:GetSelValueByKey("league") ~= standardLeagueName then - -- give up and select the first entry. Ruthless mode may not have Standard equivalents - charSelectLeague.selIndex = 1 + -- GGG's character API has an issue where for /get-characters the account name is not case-sensitive, but for /get-passive-skills and /get-items it is. + -- This workaround grabs the profile page and extracts the correct account name from one of the URLs. + launch:DownloadPage(realm.hostName .. realm.profileURL .. accountName:gsub("#", "%%23"), + function(response, errMsg) + if errMsg then + self.charImportStatus = colorCodes.NEGATIVE .. + "Error retrieving character list, try again (" .. errMsg:gsub("\n", " ") .. ")" + self.charImportMode = "GETACCOUNTNAME" + return + end + local realAccountName = response.body:match("/view%-profile/([^/]+)/characters"):gsub(".", + function(c) if c:byte(1) > 127 then return string.format("%%%2X", c:byte(1)) else return c end end) + if not realAccountName then + self.charImportStatus = colorCodes.NEGATIVE .. "Failed to retrieve character list." + self.charImportMode = "GETACCOUNTNAME" + return + end + realAccountName = realAccountName:gsub("(.*)[#%-]", "%1#") + accountName = realAccountName + self.controls.siteAccountName:SetText(realAccountName) + self.charImportStatus = "Character list successfully retrieved." + self.charImportMode = "SELECTCHAR" + self.lastRealm = realm.id + main.lastRealm = realm.id + self.lastAccountHash = common.sha1(accountName) + main.lastAccountName = accountName + main.gameAccounts[accountName] = main.gameAccounts[accountName] or {} + main.gameAccounts[accountName].sessionID = sessionID + local leagueList = {} + for i, char in ipairs(charList) do + if not isValueInArray(leagueList, char.league) then + t_insert(leagueList, char.league) + end + end + table.sort(leagueList) + charSelectLeague = self.controls.siteCharSelectLeague + wipeTable(self.controls.siteCharSelectLeague.list) + for _, league in ipairs(leagueList) do + t_insert(self.controls.siteCharSelectLeague.list, { + label = league, + league = league, + }) + end + t_insert(self.controls.siteCharSelectLeague.list, { + label = "All", + }) + -- set the league combo to the last used if possible, used for previously imported characters + if self.lastLeague then + charSelectLeague:SelByValue(self.lastLeague, "league") + -- check that it worked + if charSelectLeague:GetSelValueByKey("league") ~= self.lastLeague then + -- League maybe over, Character will be in standard + standardLeagueName = FindMatchingStandardLeague(self.lastLeague) + self.controls.siteCharSelectLeague:SelByValue(standardLeagueName, "league") + if charSelectLeague:GetSelValueByKey("league") ~= standardLeagueName then + -- give up and select the first entry. Ruthless mode may not have Standard equivalents + charSelectLeague.selIndex = 1 + else + self.lastLeague = standardLeagueName + end + end else - self.lastLeague = standardLeagueName + if self.controls.siteCharSelectLeague.selIndex > #self.controls.siteCharSelectLeague.list then + self.controls.siteCharSelectLeague.selIndex = 1 + end end - end - else - if self.controls.charSelectLeague.selIndex > #self.controls.charSelectLeague.list then - self.controls.charSelectLeague.selIndex = 1 - end - end - self.lastCharList = charList - self:BuildCharacterList(self.controls.charSelectLeague:GetSelValueByKey("league")) + self.lastCharList = charList + self:BuildCharacterList("pc", self.controls.siteCharSelectLeague:GetSelValueByKey("league"), self.lastCharList, self.controls.siteCharSelect) - -- We only get here if the accountname was correct, found, and not private, so add it to the account history. - self:SaveAccountHistory() - end, sessionID and { header = "Cookie: POESESSID=" .. sessionID }) - end, sessionID and { header = "Cookie: POESESSID=" .. sessionID }) + -- We only get here if the accountname was correct, found, and not private, so add it to the account history. + self:SaveAccountHistory() + end) + end) end -function ImportTabClass:BuildCharacterList(league) - wipeTable(self.controls.charSelect.list) - for i, char in ipairs(self.lastCharList) do - if not league or char.league == league then +--- @param realm string +--- @param league string +--- @param characters table? +--- @param control table +function ImportTabClass:BuildCharacterList(realm, league, characters, control) + wipeTable(control.list) + if not characters then + return + end + for i, char in ipairs(characters) do + if realm == char.realm and (not league) or char.league == league then charLvl = char.level or 0 charLeague = char.league or "?" charName = char.name or "?" @@ -568,31 +979,8 @@ function ImportTabClass:BuildCharacterList(league) classColor = colorCodes.DEFAULT if charClass ~= "?" then - classColor = colorCodes[charClass:upper()] - - if classColor == nil then - if (charClass == "Elementalist" or charClass == "Necromancer" or charClass == "Occultist" or - charClass == "Harbinger" or charClass == "Herald" or charClass == "Bog Shaman") then - classColor = colorCodes["WITCH"] - elseif (charClass == "Guardian" or charClass == "Inquisitor" or charClass == "Hierophant" or - charClass == "Architect of Chaos" or charClass == "Polytheist" or charClass == "Puppeteer") then - classColor = colorCodes["TEMPLAR"] - elseif (charClass == "Assassin" or charClass == "Trickster" or charClass == "Saboteur" or - charClass == "Surfcaster" or charClass == "Servant of Arakaali" or charClass == "Blind Prophet") then - classColor = colorCodes["SHADOW"] - elseif (charClass == "Gladiator" or charClass == "Slayer" or charClass == "Champion" or - charClass == "Gambler" or charClass == "Paladin" or charClass == "Aristocrat") then - classColor = colorCodes["DUELIST"] - elseif (charClass == "Raider" or charClass == "Pathfinder" or charClass == "Deadeye" or charClass == "Warden" or - charClass == "Daughter of Oshabi" or charClass == "Whisperer" or charClass == "Wildspeaker") then - classColor = colorCodes["RANGER"] - elseif (charClass == "Juggernaut" or charClass == "Berserker" or charClass == "Chieftain" or - charClass == "Antiquarian" or charClass == "Behemoth" or charClass == "Ancestral Commander") then - classColor = colorCodes["MARAUDER"] - elseif (charClass == "Ascendant" or charClass == "Reliquarian" or charClass == "Scavenger") then - classColor = colorCodes["SCION"] - end - end + local tree = main:LoadTree(latestTreeVersion .. (char.league:match("Ruthless") and "_ruthless" or "")) + classColor = colorCodes[charClass:upper()] or colorCodes[tree.ascendNameMap[charClass].class.name:upper()] or "^7" end local detail @@ -601,7 +989,7 @@ function ImportTabClass:BuildCharacterList(league) else detail = string.format("%s%s ^x808080lvl %d", classColor, charClass, charLvl) end - t_insert(self.controls.charSelect.list, { + t_insert(control.list, { label = charName, char = char, searchFilter = charName.." "..charClass, @@ -609,120 +997,77 @@ function ImportTabClass:BuildCharacterList(league) }) end end - table.sort(self.controls.charSelect.list, function(a,b) + table.sort(control.list, function(a,b) return a.char.name:lower() < b.char.name:lower() end) - self.controls.charSelect.selIndex = 1 + control.selIndex = 1 if self.lastCharacterHash then - for i, char in ipairs(self.controls.charSelect.list) do + for i, char in ipairs(control.list) do if common.sha1(char.char.name) == self.lastCharacterHash then - self.controls.charSelect.selIndex = i + control.selIndex = i break end end end end - -function ImportTabClass:SaveAccountHistory() - if not historyList[self.controls.accountName.buf] then - t_insert(historyList, self.controls.accountName.buf) - historyList[self.controls.accountName.buf] = true - table.sort(historyList, function(a,b) - return a:lower() < b:lower() - end) - self.controls.accountHistory:CheckDroppedWidth(true) - self.controls.accountHistory:SelByValue(self.controls.accountName.buf) - end -end - -function ImportTabClass:DownloadPassiveTree() - self.charImportMode = "IMPORTING" - self.charImportStatus = "Retrieving character passive tree..." - local realm = realmList[self.controls.accountRealm.selIndex] - local accountName = self.controls.accountName.buf - local sessionID = #self.controls.sessionInput.buf == 32 and self.controls.sessionInput.buf or (main.gameAccounts[accountName] and main.gameAccounts[accountName].sessionID) - local charSelect = self.controls.charSelect - local charData = charSelect.list[charSelect.selIndex].char - launch:DownloadPage(realm.hostName.."character-window/get-passive-skills?accountName="..accountName:gsub("#", "%%23").."&character="..urlEncode(charData.name).."&realm="..realm.realmCode, function(response, errMsg) - self.charImportMode = "SELECTCHAR" - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error importing character data, try again ("..errMsg:gsub("\n"," ")..")" - return - elseif response.body == "false" then - self.charImportStatus = colorCodes.NEGATIVE.."Failed to retrieve character data, try again." - return - end - self.lastCharacterHash = common.sha1(charData.name) - if not self.lastLeague then - self.lastLeague = charSelectLeague:GetSelValueByKey("league") - end - self:ImportPassiveTreeAndJewels(response.body, charData) - end, sessionID and { header = "Cookie: POESESSID=" .. sessionID }) -end - -function ImportTabClass:DownloadItems() - self.charImportMode = "IMPORTING" - self.charImportStatus = "Retrieving character items..." - local realm = realmList[self.controls.accountRealm.selIndex] - local accountName = self.controls.accountName.buf - local sessionID = #self.controls.sessionInput.buf == 32 and self.controls.sessionInput.buf or (main.gameAccounts[accountName] and main.gameAccounts[accountName].sessionID) - local charSelect = self.controls.charSelect - local charData = charSelect.list[charSelect.selIndex].char - launch:DownloadPage(realm.hostName.."character-window/get-items?accountName="..accountName:gsub("#", "%%23").."&character="..urlEncode(charData.name).."&realm="..realm.realmCode, function(response, errMsg) - self.charImportMode = "SELECTCHAR" - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error importing character data, try again ("..errMsg:gsub("\n"," ")..")" - return - elseif response.body == "false" then - self.charImportStatus = colorCodes.NEGATIVE.."Failed to retrieve character data, try again." - return - end - self.lastCharacterHash = common.sha1(charData.name) - if not self.lastLeague then - self.lastLeague = charSelectLeague:GetSelValueByKey("league") - end - self:ImportItemsAndSkills(response.body) - end, sessionID and { header = "Cookie: POESESSID=" .. sessionID }) -end - -function ImportTabClass:ImportPassiveTreeAndJewels(json, charData) - --local out = io.open("get-passive-skills.json", "w") - --out:write(json) - --out:close() - local charPassiveData, errMsg = self:ProcessJSON(json) - --local out = io.open("get-passive-skills.json", "w") - --writeLuaTable(out, charPassiveData, 1) - --out:close() - - -- 3.16+ - if charPassiveData.mastery_effects then - local mastery, effect = 0, 0 - for key, value in pairs(charPassiveData.mastery_effects) do - if type(value) ~= "string" then - break - end - mastery = band(tonumber(value), 65535) - effect = b_rshift(tonumber(value), 16) - t_insert(charPassiveData.mastery_effects, mastery, effect) - end - end - - if charPassiveData.skill_overrides then - for nodeId, override in pairs(charPassiveData.skill_overrides) do - self.build.spec:ReplaceNode(override, self.build.spec.tree.tattoo.nodes[override.name]) - override.id = nodeId - end - end - - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error processing character data, try again later." - return - end - self.charImportStatus = colorCodes.POSITIVE.."Passive tree and jewels successfully imported." - self.build.spec.jewel_data = copyTable(charPassiveData.jewel_data) - self.build.spec.extended_hashes = copyTable(charPassiveData.hashes_ex) - --ConPrintTable(charPassiveData) - if self.controls.charImportTreeClearJewels.state then +-- https://www.pathofexile.com/developer/docs/reference#type-Character +--- @class CharacterBasicData +--- @field id string? not present on website +--- @field name string +--- @field realm "pc" | "xbox" | "sony" +--- @field class string +--- @field league string +--- @field level integer +--- @field experience integer +--- +--- @alias Bandits "Kraityn" | "Alira" | "Oak" | "Eramir" +--- @alias MajorPantheon "TheBrineKing" | " Arakaali" | " Solaris" | "Lunaris" +--- @alias MinorPantheon "Abberath" | " Gruthkul" | " Yugul" | " Shakari" | " Tukohama" | " Ralakesh" | " Garukhan" | "Ryslatha" + + +--- @class CharacterPassives +--- @field mastery_effects table +--- @field skill_overrides table +--- @field jewel_data table +--- @field hashes_ex integer[] +--- @field hashes integer[] +--- @field bandit_choice Bandits? +--- @field pantheon_major MajorPantheon? +--- @field pantheon_minor MinorPantheon? +--- @field alternate_ascendancy string | integer integer on website, string on oauth + +-- https://www.pathofexile.com/developer/docs/reference#type-Item +--- @alias Item any + +--- @class CharacterPassivesData : CharacterBasicData +--- @field jewels Item[] +--- @field passives CharacterPassives +--- @param charData CharacterPassivesData +--- @param deleteJewels boolean +--- @return string +function ImportTabClass:ImportPassiveTreeAndJewels(charData, deleteJewels) + local charPassives = copyTable(charData.passives) + + -- fix table keys being strings + local masteries = {} + for key, value in pairs(charPassives.mastery_effects) do + masteries[tonumber(key)] = value + end + self.build.spec.jewel_data = {} + for key, value in pairs(charPassives.jewel_data) do + self.build.spec.jewel_data[tonumber(key)] = value + end + local skillOverrides = {} + for nodeId, override in pairs(charPassives.skill_overrides) do + -- json keys are strings, not numbers + local nodeIdNum = tonumber(nodeId) + self.build.spec:ReplaceNode(override, self.build.spec.tree.tattoo.nodes[override.name]) + override.id = nodeIdNum + skillOverrides[nodeIdNum] = override + end + + self.build.spec.extended_hashes = copyTable(charPassives.hashes_ex) + if deleteJewels then for _, slot in pairs(self.build.itemsTab.slots) do if slot.selItemId ~= 0 and slot.nodeId then self.build.itemsTab.build.spec.ignoreAllocatingSubgraph = true -- ignore allocated cluster nodes on Import when Delete Jewel is true, clean slate @@ -730,46 +1075,41 @@ function ImportTabClass:ImportPassiveTreeAndJewels(json, charData) end end end - for _, itemData in pairs(charPassiveData.items) do + for _, itemData in ipairs(charData.jewels) do self:ImportItem(itemData) end self.build.itemsTab:PopulateSlots() self.build.itemsTab:AddUndoState() - -- Alternate trees don't have an identifier, so we're forced to look up something that is unique to that tree - -- Hopefully this changes, because it's totally unmaintainable - local function isAscendancyInTree(className, treeVersion) - local classes = main.tree[treeVersion].classes - for _, class in pairs(classes) do - if class.name == className then - return true - end - for i = 0, #class.classes do - local ascendClass = class.classes[i] - if ascendClass.name == className then - return true - end - end + + local alternateAscendancyId + if charPassives.alternate_ascendancy then + -- oauth responses have bloodline names + if type(charPassives.alternate_ascendancy) == "string" then + local bloodline = self.build.latestTree.secondaryAscendNameMap[charPassives.alternate_ascendancy] + alternateAscendancyId = bloodline and bloodline.ascendClassId + -- site responses have integer ids + else + alternateAscendancyId = charPassives.alternate_ascendancy end + else + alternateAscendancyId = 0 end - -- Character import uses current GGG cluster hashes. self.build.spec.clusterHashFormatVersion = 2 - self.build.spec:ImportFromNodeList(charPassiveData.character, - charPassiveData.ascendancy, - charPassiveData.alternate_ascendancy or 0, - charPassiveData.hashes, - charPassiveData.skill_overrides, - charPassiveData.mastery_effects or {}, - latestTreeVersion .. (charData.league:match("Ruthless") and "_ruthless" or "") .. (isAscendancyInTree(charData.class, latestTreeVersion) and "" or "_alternate") - ) + self.build.spec:ImportFromNodeList(charData.class, + nil, + nil, + alternateAscendancyId, + charPassives.hashes, + skillOverrides, + masteries, + latestTreeVersion .. (charData.league:match("Ruthless") and "_ruthless" or "") + ) self.build.treeTab:SetActiveSpec(self.build.treeTab.activeSpec) self.build.spec:BuildClusterJewelGraphs() self.build.spec:AddUndoState() - if not self.lastLeague then - self.lastLeague = charSelectLeague:GetSelValueByKey("league") - end - self.build.characterLevel = charData.level + self.build.characterLevel = charData.level or 100 self.build.characterLevelAutoMode = false self.build.configTab:UpdateLevel() self.build.controls.characterLevel:SetText(charData.level) @@ -784,7 +1124,31 @@ function ImportTabClass:ImportPassiveTreeAndJewels(json, charData) end end self.build.configTab.varControls["resistancePenalty"]:SetSel(resistancePenaltyIndex) - main:SetWindowTitleSubtext(string.format("%s (%s, %s, %s)", self.build.buildName, charData.name, charData.class, charData.league)) + + local function setSelByVal(dropdown, val) + for i, v in ipairs(dropdown.list) do + if v.val == val then + dropdown:SetSel(i) + end + end + end + + local bandit = (charPassives.bandit_choice == "Eramir" or not charPassives.bandit_choice) and "None" or + charPassives.bandit_choice + setSelByVal(self.build.configTab.varControls["bandit"], + bandit) + + local majorGod = charPassives.pantheon_major or "None" + setSelByVal(self.build.configTab.varControls["pantheonMajorGod"], + majorGod) + + local minorGod = charPassives.pantheon_minor or "None" + setSelByVal(self.build.configTab.varControls["pantheonMinorGod"], + minorGod) + + main:SetWindowTitleSubtext(string.format("%s (%s, %s, %s)", self.build.buildName, charData.name, charData.class, + charData.league)) + return colorCodes.POSITIVE.."Passive tree and jewels successfully imported." end local SOCKET_GROUP_REIMPORT_KEY_SEPARATOR = "\31" @@ -872,16 +1236,16 @@ local function applySocketGroupReimportState(socketGroup, state) end end -function ImportTabClass:ImportItemsAndSkills(json) - --local out = io.open("get-items.json", "w") - --out:write(json) - --out:close() - local charItemData, errMsg = self:ProcessJSON(json) - if errMsg then - self.charImportStatus = colorCodes.NEGATIVE.."Error processing character data, try again later." - return - end - if self.controls.charImportItemsClearItems.state then +--- @class CharacterItemsData : CharacterBasicData +--- @field equipment Item[] +--- @param charData CharacterItemsData +--- @param clearItems boolean +--- @param clearSkills boolean +--- @param ignoreWeaponSwap boolean +--- @return CharacterItemsData, string +function ImportTabClass:ImportItemsAndSkills(charData, clearItems, clearSkills, ignoreWeaponSwap) + charData = copyTable(charData) + if clearItems then for _, slot in pairs(self.build.itemsTab.slots) do if slot.selItemId ~= 0 and not slot.nodeId then self.build.itemsTab:DeleteItem(self.build.itemsTab.items[slot.selItemId]) @@ -892,7 +1256,7 @@ function ImportTabClass:ImportItemsAndSkills(json) local mainSkillEmpty = #self.build.skillsTab.socketGroupList == 0 local skillOrder local preservedSocketGroupStateByKey - if self.controls.charImportItemsClearSkills.state then + if clearSkills then skillOrder = { } preservedSocketGroupStateByKey = { } for _, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do @@ -910,10 +1274,8 @@ function ImportTabClass:ImportItemsAndSkills(json) wipeTable(self.build.skillsTab.socketGroupList) self.build.skillsTab:RebuildImbuedSupportBySlot() end - self.charImportStatus = colorCodes.POSITIVE.."Items and skills successfully imported." - --ConPrintTable(charItemData) - for _, itemData in pairs(charItemData.items) do - self:ImportItem(itemData) + for _, itemData in ipairs(charData.equipment) do + self:ImportItem(itemData, nil, ignoreWeaponSwap) end if skillOrder then local groupOrder = { } @@ -974,24 +1336,25 @@ function ImportTabClass:ImportItemsAndSkills(json) self.build.itemsTab:PopulateSlots() self.build.itemsTab:AddUndoState() self.build.skillsTab:AddUndoState() - self.build.characterLevel = charItemData.character.level + self.build.characterLevel = charData.level self.build.configTab:UpdateLevel() - self.build.controls.characterLevel:SetText(charItemData.character.level) + self.build.controls.characterLevel:SetText(tostring(charData.level)) self.build.buildFlag = true - return charItemData.character -- For the wrapper + -- charData for the wrapper + return charData, colorCodes.POSITIVE .. "Items and skills successfully imported." end local rarityMap = { [0] = "NORMAL", "MAGIC", "RARE", "UNIQUE", [9] = "RELIC", [10] = "RELIC" } local slotMap = { ["Weapon"] = "Weapon 1", ["Offhand"] = "Weapon 2", ["Weapon2"] = "Weapon 1 Swap", ["Offhand2"] = "Weapon 2 Swap", ["Helm"] = "Helmet", ["BodyArmour"] = "Body Armour", ["Gloves"] = "Gloves", ["Boots"] = "Boots", ["Amulet"] = "Amulet", ["Ring"] = "Ring 1", ["Ring2"] = "Ring 2", ["Ring3"] = "Ring 3", ["Belt"] = "Belt", ["BrequelGrafts"] = "Graft 1", ["BrequelGrafts2"] = "Graft 2", } -function ImportTabClass:ImportItem(itemData, slotName) +function ImportTabClass:ImportItem(itemData, slotName, ignoreWeaponSwap) if not slotName then if itemData.inventoryId == "PassiveJewels" then slotName = "Jewel "..self.build.latestTree.jewelSlots[itemData.x + 1] elseif itemData.inventoryId == "Flask" then slotName = "Flask "..(itemData.x + 1) - elseif not (self.controls.charImportItemsIgnoreWeaponSwap.state and (itemData.inventoryId == "Weapon2" or itemData.inventoryId == "Offhand2")) then + elseif not (ignoreWeaponSwap and (itemData.inventoryId == "Weapon2" or itemData.inventoryId == "Offhand2")) then slotName = slotMap[itemData.inventoryId] end end @@ -1382,23 +1745,10 @@ function UrlDecode(url) return url end -function ImportTabClass:ProcessJSON(json) - local func, errMsg = loadstring("return "..jsonToLua(json)) - if errMsg then - return nil, errMsg - end - setfenv(func, { }) -- Sandbox the function just in case - local data = func() - if type(data) ~= "table" then - return nil, "Return type is not a table" - end - return data -end - function ImportTabClass:SetPredefinedBuildName() - local accountName = self.controls.accountName.buf:gsub('%s+', ''):gsub("#%d+", "") - local charSelect = self.controls.charSelect + local accountName = self.controls.siteAccountName.buf:gsub('%s+', ''):gsub("#%d+", "") + local charSelect = self.controls.siteCharSelect local charData = charSelect.list[charSelect.selIndex].char local charName = charData.name - main.predefinedBuildName = accountName.." - "..charName + main.predefinedBuildName = accountName .. " - " .. charName end diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e28e74a86b..38ebfbb8ae 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -176,7 +176,7 @@ function PassiveSpecClass:Load(xml, dbFileName) end end end - self:ImportFromNodeList(tonumber(xml.attrib.classId), tonumber(xml.attrib.ascendClassId), tonumber(xml.attrib.secondaryAscendClassId or 0), hashList, self.hashOverrides, masteryEffects) + self:ImportFromNodeList(nil, tonumber(xml.attrib.classId), tonumber(xml.attrib.ascendClassId), tonumber(xml.attrib.secondaryAscendClassId or 0), hashList, self.hashOverrides, masteryEffects) elseif url then self:DecodeURL(url) end @@ -242,13 +242,20 @@ function PassiveSpecClass:PostLoad() end -- Import passive spec from the provided class IDs and node hash list -function PassiveSpecClass:ImportFromNodeList(classId, ascendClassId, secondaryAscendClassId, hashList, hashOverrides, masteryEffects, treeVersion) +function PassiveSpecClass:ImportFromNodeList(className, classId, ascendClassId, secondaryAscendClassId, hashList, hashOverrides, masteryEffects, treeVersion) if hashOverrides == nil then hashOverrides = {} end if treeVersion and treeVersion ~= self.treeVersion then self:Init(treeVersion) self.build.treeTab.showConvert = self.treeVersion ~= latestTreeVersion end self:ResetNodes() + if className then + classId = self.tree.classNameMap[className] or + (self.tree.ascendNameMap[className] and self.tree.ascendNameMap[className].classId) or + (self.tree.internalAscendNameMap[className] and self.tree.internalAscendNameMap[className].classId) + ascendClassId = (self.tree.ascendNameMap[className] and self.tree.ascendNameMap[className].ascendClassId) or + (self.tree.internalAscendNameMap[className] and self.tree.internalAscendNameMap[className].ascendClassId) or 0 + end self:SelectClass(classId) self:SelectAscendClass(ascendClassId) self:SelectSecondaryAscendClass(secondaryAscendClassId) @@ -1922,7 +1929,7 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe if proxyGroup == data.group then if node.oidx == data.orbitIndex and not data.isMastery then for _, extendedId in ipairs(importedGroups[proxyGroup].nodes) do - if id == tonumber(extendedId) and inExtendedHashes(id) then + if id == extendedId and inExtendedHashes(tonumber(id)) then return true end end @@ -2248,7 +2255,7 @@ function PassiveSpecClass:CreateUndoState() end function PassiveSpecClass:RestoreUndoState(state, treeVersion) - self:ImportFromNodeList(state.classId, state.ascendClassId, state.secondaryAscendClassId, state.hashList, state.hashOverrides, state.masteryEffects, treeVersion or state.treeVersion) + self:ImportFromNodeList(nil, state.classId, state.ascendClassId, state.secondaryAscendClassId, state.hashList, state.hashOverrides, state.masteryEffects, treeVersion or state.treeVersion) self:SetWindowTitleWithBuildClass() end diff --git a/src/Classes/PassiveTree.lua b/src/Classes/PassiveTree.lua index 6ea7da09a0..3e0314643e 100644 --- a/src/Classes/PassiveTree.lua +++ b/src/Classes/PassiveTree.lua @@ -103,7 +103,8 @@ local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion) -- Build maps of class name -> class table self.classNameMap = { } self.ascendNameMap = { } - self.classNotables = { } + self.internalAscendNameMap = {} + self.classNotables = {} for classId, class in pairs(self.classes) do if versionNum >= 3.10 then @@ -121,6 +122,14 @@ local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion) flavourText = ascendClass.flavourText, flavourTextRect = ascendClass.flavourTextRect, } + if ascendClass.internalId then + self.internalAscendNameMap[ascendClass.internalId] = { + classId = classId, + class = class, + ascendClassId = ascendClassId, + ascendClass = ascendClass + } + end end end diff --git a/src/Classes/PoEAPI.lua b/src/Classes/PoEAPI.lua new file mode 100644 index 0000000000..6418545af5 --- /dev/null +++ b/src/Classes/PoEAPI.lua @@ -0,0 +1,217 @@ +local base64 = require("base64") +local sha = require("sha2") +local dkjson = require "dkjson" + +local scopesOAuth = { + "account:profile", + "account:leagues", + "account:characters", + "account:trade" +} + +local filename = "poe_api_response.json" + +local PoEAPIClass = newClass("PoEAPI", function(self, authToken, refreshToken, tokenExpiry) + self.retries = 0 + self.authToken = authToken + self.refreshToken = refreshToken + self.tokenExpiry = tokenExpiry or 0 + self.baseUrl = "https://api.pathofexile.com" + self.rateLimiter = new("TradeQueryRateLimiter") + + self.ERROR_NO_AUTH = "No auth token" +end) + + +--- @param callback fun(valid: bool, updateSettings: bool) +function PoEAPIClass:ValidateAuth(callback) + -- make a call for profile if not error we are good + -- if error 401 then try to recreate the token with + if self.authToken and self.refreshToken and self.tokenExpiry then + ConPrintf("Validating auth token") + if self.tokenExpiry < os.time() then + ConPrintf("Auth token expired") + -- here recreate the token with the refresh_token + local formText = "client_id=pob&grant_type=refresh_token&refresh_token=" .. self.refreshToken + launch:DownloadPage("https://www.pathofexile.com/oauth/token", function(response, errMsg) + ConPrintf("Recreating auth token") + if errMsg then + ConPrintf("Failed to recreate auth token: %s", errMsg) + callback(false, false) + return + end + local responseLua = dkjson.decode(response.body) + self.authToken = responseLua.access_token + self.refreshToken = responseLua.refresh_token + self.tokenExpiry = os.time() + responseLua.expires_in + self:UpdateMain() + self.retries = 0 + callback(true, true) + end, { body = formText }) + else + callback(true, false) + end + else + callback(false, false) + end +end + +--- @param secret string +local function base64_encode(secret) + return base64.encode(secret):gsub("+", "-"):gsub("/", "_"):gsub("=$", "") +end + +--- resets current authorization details +function PoEAPIClass:ResetDetails() + self.authToken = nil + self.refreshToken = nil + self.tokenExpiry = nil + self:UpdateMain() +end + +--- updates main so that API details are saved across restarts +function PoEAPIClass:UpdateMain() + main.lastToken = self.authToken + main.lastRefreshToken = self.refreshToken + main.tokenExpiry = self.tokenExpiry + main:SaveSettings() +end + +--- @param callback fun(errCode: string?) +function PoEAPIClass:FetchAuthToken(callback) + math.randomseed(os.time()) + local secret = math.random(2 ^ 32 - 1) + local code_verifier = base64_encode(tostring(secret)) + local code_challenge = base64_encode(sha.hex_to_bin(sha.sha256(code_verifier))) + + -- 16 character hex string + local initialState = string.gsub('xxxxxxxxxxxxxxxx', 'x', function() + return string.format('%x', math.random(0, 0xf)) + end) + + local authUrl = string.format( + "https://www.pathofexile.com/oauth/authorize?client_id=pob&response_type=code&scope=%s&state=%s&code_challenge=%s&code_challenge_method=S256" + , table.concat(scopesOAuth, "%20") + , initialState + , code_challenge + ) + + local server = io.open("LaunchServer.lua", "r") + local id = LaunchSubScript(server:read("*a"), "", "ConPrintf,OpenURL", authUrl) + if id then + launch.subScripts[id] = { + type = "DOWNLOAD", + callback = function(code, errMsg, state, port) + if not code then + ConPrintf("Failed to get code from server: %s", errMsg) + self:ResetDetails() + callback(self.ERROR_NO_AUTH) + return + end + + if initialState ~= state then + return + end + local formText = "client_id=pob&grant_type=authorization_code&code=" .. + code .. + "&redirect_uri=http://localhost:" .. + port .. "&scope=" .. table.concat(scopesOAuth, " ") .. "&code_verifier=" .. code_verifier + launch:DownloadPage("https://www.pathofexile.com/oauth/token", function(response, errMsg) + if errMsg then + ConPrintf("Failed to get token from server: " .. errMsg) + self:ResetDetails() + callback() + return + end + local responseLua = dkjson.decode(response.body) + self.authToken = responseLua.access_token + self.refreshToken = responseLua.refresh_token + self.tokenExpiry = os.time() + responseLua.expires_in + self:UpdateMain() + self.retries = 0 + SetForeground() + callback() + end, { body = formText }) + end + } + end +end + +--- @param endpoint string +--- @param callback fun(response: table?, errorMsg: string, updateSettings: bool) +function PoEAPIClass:DownloadWithRefresh(endpoint, callback) + self:ValidateAuth(function(valid, updateSettings) + if not valid then + -- Clean info about token and refresh token + self:ResetDetails() + callback(nil, self.ERROR_NO_AUTH, true) + return + end + + launch:DownloadPage(self.baseUrl .. endpoint, function(response, errMsg) + if errMsg and errMsg:match("401") and self.retries < 1 then + -- try once again with refresh token + self.retries = 1 + self.tokenExpiry = 0 + self:DownloadWithRefresh(endpoint, callback) + else + self.retries = 0 + if errMsg then + ConPrintf("Failed to download %s: %s", endpoint, errMsg) + elseif response and response.body then + -- create the file and log the name file + local file = io.open(filename, "w") + if file then + file:write(response.body) + file:close() + end + ConPrintf("Download %s:\n%s\n", endpoint, filename) + end + callback(response, errMsg, updateSettings) + end + end, { header = "Authorization: Bearer " .. self.authToken }) + end) +end + +--- @alias DownloadCallback fun(body: table?, err: string?, timeout: integer?) +--- @param policy string +--- @param url string +--- @param callback DownloadCallback +function PoEAPIClass:DownloadWithRateLimit(policy, url, callback) + local now = os.time() + local timeNext = self.rateLimiter:NextRequestTime(policy, now) + if now >= timeNext then + local requestId = self.rateLimiter:InsertRequest(policy) + local onComplete = function(response, errMsg) + self.rateLimiter:FinishRequest(policy, requestId) + self.rateLimiter:UpdateFromHeader(response.header, policy) + if response.header:match("HTTP/[%d%.]+ (%d+)") == "429" then + timeNext = self.rateLimiter:NextRequestTime(policy, now) + callback(nil, "Response code: 429", timeNext) + return + end + local responseLua = dkjson.decode(response.body) + callback(responseLua, errMsg, nil) + end + self:DownloadWithRefresh(url, onComplete) + else + callback(nil, "Response code: 429", timeNext) + end +end + +---Fetches character list from PoE's OAuth api +---@param realm string Realm to fetch the list from +---@param callback DownloadCallback +function PoEAPIClass:DownloadCharacterList(realm, callback) + self:DownloadWithRateLimit("character-list-request-limit", + "/character" .. (realm == "pc" and "" or "/" .. realm), callback) +end + +---Fetches character from PoE's OAuth api +---@param realm string Realm to fetch the character from +---@param name string Character name to fetch +---@param callback DownloadCallback +function PoEAPIClass:DownloadCharacter(realm, name, callback) + self:DownloadWithRateLimit("character-request-limit", + "/character" .. (realm == "pc" and "" or "/" .. realm) .. "/" .. name, callback) +end diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 9e1308bfb9..2418bb0b56 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -34,8 +34,9 @@ local TradeQueryClass = newClass("TradeQuery", function(self, itemsTab) -- default set of trade item sort selection self.slotTables = { } self.pbItemSortSelectionIndex = 1 + -- for each league, a table of values of each currency in div + --- @type table> self.pbCurrencyConversion = { } - self.currencyConversionTradeMap = { } self.lastCurrencyConversionRequest = 0 self.lastCurrencyFileTime = { } self.pbFileTimestampDiff = { } @@ -45,13 +46,44 @@ local TradeQueryClass = newClass("TradeQuery", function(self, itemsTab) -- table holding all realm/league pairs. (allLeagues[realm] = [league.id,...]) self.allLeagues = {} -- realm id-text table to pair realm name with API parameter - self.realmIds = {} + self.realmIds = { + ["PC"] = "pc", + ["Xbox"] = "xbox", + ["Sony"] = "sony" + } + --- @type integer? + self.backoffFinish = nil -- last query for each row self.lastQueries = {} self.tradeQueryRequests = new("TradeQueryRequests") + local function onRateLimit(backoff) + self.backoffFinish = get_time() + backoff + self.countDown = coroutine.create(function() + while self.backoffFinish do + local now = get_time() + if self.backoffFinish < (now + 0.5) then + self.backoffFinish = nil + self:SetNotice(self.controls.pbNotice, "") + return + end + local msg = s_format("Rate limited. Retrying after %s seconds...", self.backoffFinish - now) + self:SetNotice(self.controls.pbNotice, colorCodes.WARNING..msg) + coroutine.yield() + end + end) + end main.onFrameFuncs["TradeQueryRequests"] = function() - self.tradeQueryRequests:ProcessQueue() + self.tradeQueryRequests:ProcessQueue(onRateLimit) + if self.countDown then + coroutine.resume(self.countDown) + if coroutine.status(self.countDown) == "dead" then + self.countDown = nil + end + end + end + if not main.api then + main.api = new("PoEAPI", main.lastToken, main.lastRefreshToken, main.tokenExpiry) end -- set @@ -121,20 +153,13 @@ function TradeQueryClass:PullLeagueList() end) end --- Method to convert currency to chaos equivalent -function TradeQueryClass:ConvertCurrencyToChaos(currency, amount) - local conversionTable = self.pbCurrencyConversion[self.pbLeague] - - -- we take the ceiling of all prices to integer chaos - -- to prevent dealing with shenanigans of people asking 4.9 chaos - if conversionTable and conversionTable[currency:lower()] then - --ConPrintf("Converted '"..currency.."' at " ..tostring(conversionTable[currency:lower()])) - return m_ceil(amount * conversionTable[currency:lower()]) - elseif currency:lower() == "chaos" then - return m_ceil(amount) - else - ConPrintf("Unhandled Currency Conversion: '" .. currency:lower() .. "'") - return nil +--- @param currencyId string +--- @param amount integer +--- @return number? +function TradeQueryClass:ConvertCurrencyToDivs(currencyId, amount) + local map = self.pbCurrencyConversion[self.pbLeague] + if map and map[currencyId] then + return amount * map[currencyId] end end @@ -146,60 +171,62 @@ function TradeQueryClass:PullPoENinjaCurrencyConversion(league) self:SetNotice(self.controls.pbNotice, "PoE Ninja Rate Limit Exceeded: " .. tostring(3600 - (now - self.lastCurrencyConversionRequest))) return end - -- We are getting currency short-names from Poe API before getting PoeNinja rates - -- Potentially, currency short-names could be cached but this request runs - -- once per hour at most and the Poe API response is already Cloudflare cached - self:FetchCurrencyConversionTable(function(data, errMsg) - if errMsg then - self:SetNotice(self.controls.pbNotice, "Error: " .. tostring(errMsg)) - return - end - self.pbCurrencyConversion[league] = { } - self.lastCurrencyConversionRequest = now - launch:DownloadPage( - "https://poe.ninja/api/data/CurrencyRates?league=" .. urlEncode(league), - function(response, errMsg) - if errMsg then - self:SetNotice(self.controls.pbNotice, "Error: " .. tostring(errMsg)) - return - end - local json_data = dkjson.decode(response.body) - if not json_data then - self:SetNotice(self.controls.pbNotice, "Failed to Get PoE Ninja response") - return - end - self:PriceBuilderProcessPoENinjaResponse(json_data, self.controls) - local print_str = "" - for key, value in pairs(self.pbCurrencyConversion[self.pbLeague]) do - print_str = print_str .. '"'..key..'": '..tostring(value)..',' - end - local foo = io.open("../"..self.pbLeague.."_currency_values.json", "w") - foo:write("{" .. print_str .. '"updateTime": ' .. tostring(get_time()) .. "}") - foo:close() - self:SetCurrencyConversionButton() - end) - end) + + self.pbCurrencyConversion[league] = { } + self.lastCurrencyConversionRequest = now + launch:DownloadPage( + "https://poe.ninja/poe1/api/economy/exchange/current/overview?type=Currency&league=" .. urlEncode(league), + function(response, errMsg) + if errMsg then + self:SetNotice(self.controls.pbNotice, "Error: " .. tostring(errMsg)) + return + end + local json_data = dkjson.decode(response.body) + if not json_data or not json_data.lines then + self:SetNotice(self.controls.pbNotice, "Failed to Get PoE Ninja response") + return + end + if not self:PriceBuilderProcessPoENinjaResponse(json_data.lines) then + -- don't edit json on failure + return + end + local print_str = "" + for key, value in pairs(self.pbCurrencyConversion[self.pbLeague]) do + print_str = print_str .. '"'..key..'": '..tostring(value)..',' + end + local foo = io.open("../"..self.pbLeague.."_currency_values.json", "w") + foo:write("{" .. print_str .. '"updateTime": ' .. tostring(get_time()) .. "}") + foo:close() + self:SetCurrencyConversionButton() + end) + end -- Method to process the PoE.Ninja response -function TradeQueryClass:PriceBuilderProcessPoENinjaResponse(resp) - if resp then - -- Populate the chaos-converted values for each tradeId - for currencyName, chaosEquivalent in pairs(resp) do - local currencyName = currencyName:lower() - if self.currencyConversionTradeMap[currencyName] then - self.pbCurrencyConversion[self.pbLeague][self.currencyConversionTradeMap[currencyName]] = chaosEquivalent - else - ConPrintf("Unhandled Currency Name: '"..currencyName.."'") - end - end - -- if nothing was actually found, we should add a notice - if next(self.pbCurrencyConversion[self.pbLeague]) == nil then - self:SetNotice(self.controls.pbNotice, "No currencies received from PoE Ninja") +--- @param responseLines table[] +--- @return bool +function TradeQueryClass:PriceBuilderProcessPoENinjaResponse(responseLines) + -- Populate the divine-converted values for each tradeId + for _, currencyDetails in ipairs(responseLines) do + -- these use the same ids as the trade site, which are also short + -- readable names, like "transmute" or "aug", which means there's no + -- need for conversion. + local id = currencyDetails.id + -- poe.ninja uses divs as the primary currency, and as far as I know, + -- this figure is equivalent to the best ratio in equivalent divs + local divs = currencyDetails.primaryValue + if not id or not divs then + self:SetNotice(self.controls.pbNotice, "Currencies not updated: malformed PoE Ninja response") + return false end - else - self:SetNotice(self.controls.pbNotice, "PoE Ninja JSON Processing Error") + self.pbCurrencyConversion[self.pbLeague][id] = divs + end + -- if nothing was actually found, we should add a notice + if next(self.pbCurrencyConversion[self.pbLeague]) == nil then + self:SetNotice(self.controls.pbNotice, "No currencies received from PoE Ninja") + return false end + return true end local function initStatSortSelectionList(list) @@ -250,35 +277,73 @@ function TradeQueryClass:PriceItem() return #self.itemsTab.itemSetOrderList > 1 end - self.controls.poesessidButton = new("ButtonControl", {"TOPLEFT", self.controls.setSelect, "TOPLEFT"}, {0, row_height + row_vertical_padding, 188, row_height}, function() return main.POESESSID ~= "" and "^2Session Mode" or colorCodes.WARNING.."No Session Mode" end, function() - local poesessid_controls = {} - poesessid_controls.sessionInput = new("EditControl", nil, {0, 18, 350, 18}, main.POESESSID, nil, "%X", 32) - poesessid_controls.sessionInput:SetProtected(true) - poesessid_controls.sessionInput.placeholder = "Enter your session ID here" - poesessid_controls.sessionInput.tooltipText = "You can get this from your web browser's cookies while logged into the Path of Exile website." - poesessid_controls.save = new("ButtonControl", {"TOPRIGHT", poesessid_controls.sessionInput, "TOP"}, {-8, 24, 90, row_height}, "Save", function() - main.POESESSID = poesessid_controls.sessionInput.buf - main:ClosePopup() - main:SaveSettings() - self:UpdateRealms() - end) - poesessid_controls.save.enabled = function() return #poesessid_controls.sessionInput.buf == 32 or poesessid_controls.sessionInput.buf == "" end - poesessid_controls.cancel = new("ButtonControl", {"TOPLEFT", poesessid_controls.sessionInput, "TOP"}, {8, 24, 90, row_height}, "Cancel", function() - main:ClosePopup() + self.loginStatus = function() + if main.api.authToken then + self.clickTime = nil + return "Authenticated" + elseif self.clickTime then + local left = m_max(0,(self.clickTime + 30) - os.time()) + if left == 0 then + self.clickTime = nil + return "Not authenticated" + else + return "Logging in... (" .. left .. ")" + end + else + return colorCodes.WARNING.."Not authenticated" + end + end + + if main.api.authToken then + main.api:ValidateAuth(function(valid, _updateSettings) + if valid then + return + else + main.api:ResetDetails() + end end) - main:OpenPopup(364, 72, "Change session ID", poesessid_controls) + end + self.controls.poesessidButton = new("ButtonControl", {"TOPLEFT", self.controls.setSelect, "TOPLEFT"}, {0, row_height + row_vertical_padding, 188, row_height}, self.loginStatus, function() + -- LOGIN + if not main.api.authToken then + main.api:FetchAuthToken(function() + if main.api.authToken then + self.loginStatus = "Authenticated" + + main.lastToken = main.api.authToken + main.lastRefreshToken = main.api.refreshToken + main.tokenExpiry = main.api.tokenExpiry + main:SaveSettings() + + TradeQueryClass:SetNotice(self.controls.pbNotice, "") + else + self.loginStatus = colorCodes.WARNING.."Not authenticated" + end + end) + self.clickTime = os.time() + -- LOGOUT + else + main.lastToken = nil + main.api.authToken = nil + main.lastRefreshToken = nil + main.api.refreshToken = nil + main.tokenExpiry = nil + main.api.tokenExpiry = nil + main:SaveSettings() + end end) self.controls.poesessidButton.tooltipText = [[ -The Trader feature supports two modes of operation depending on the POESESSID availability. -You can click this button to enter your POESESSID. +The Trader feature supports two modes of operation depending on the authorization availability. +You can click this button to authorize PoB by logging in. ^2Session Mode^7 -- Requires POESESSID. +- Requires authorization on pathofexile.com. - You can search, compare, and quickly import items without leaving Path of Building. +- You can select an item and search it directly. - You can generate and perform searches for the private leagues you are participating. ^xFF9922No Session Mode^7 -- Doesn't require POESESSID. +- Doesn't require authorization. - You cannot search and compare items in Path of Building. - You can generate weighted search URLs but have to visit the trade site and manually import items. - You can only generate weighted searches for public leagues. (Generated searches can be modified @@ -292,7 +357,7 @@ on trade site to work on other leagues and realms)]] "In person (online)", "Any (includes offline)" } - + self.controls.tradeTypeSelection = new("DropDownControl", { "TOPLEFT", self.controls.poesessidButton, "BOTTOMLEFT" }, { 0, row_vertical_padding, 188, row_height }, self.tradeTypes, function(index, value) self.tradeTypeIndex = index @@ -302,7 +367,7 @@ on trade site to work on other leagues and realms)]] -- Fetches Box self.maxFetchPerSearchDefault = 2 - self.controls.fetchCountEdit = new("EditControl", {"TOPRIGHT", nil, "TOPRIGHT"}, {-12, 19, 154, row_height}, "", "Fetch Pages", "%D", 3, function(buf) + self.controls.fetchCountEdit = new("EditControl", {"TOPRIGHT", nil, "TOPRIGHT"}, {-12, 19, 150, row_height}, "", "Fetch Pages", "%D", 3, function(buf) self.maxFetchPages = m_min(m_max(tonumber(buf) or self.maxFetchPerSearchDefault, 1), 10) self.tradeQueryRequests.maxFetchPerSearch = 10 * self.maxFetchPages self.controls.fetchCountEdit.focusValue = self.maxFetchPages @@ -464,7 +529,27 @@ Highest Weight - Displays the order retrieved from trade]] t_insert(slotTables, { slotName = self.itemsTab.sockets[nodeId].label, nodeId = nodeId }) end - self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, row_vertical_padding + row_height, 0, 0}, "") + self.controls.authenticateButton = new("ButtonControl", {"TOPLEFT",self.controls.characterImportAnchor,"TOPLEFT"}, {0, 0, 200, 16}, "^7Authorize with Path of Exile", function() + main.api:FetchAuthToken(function() + if main.api.authToken then + self.charImportStatus = "Authenticated" + + main.lastToken = main.api.authToken + main.lastRefreshToken = main.api.refreshToken + main.tokenExpiry = main.api.tokenExpiry + main:SaveSettings() + else + self.charImportStatus = colorCodes.WARNING.."Not authenticated" + end + end) + local clickTime = os.time() + self.charImportStatus = function() return "Logging in... (" .. m_max(0, (clickTime + 30) - os.time()) .. ")" end + end) + self.controls.authenticateButton.shown = function() + return self.charImportMode == "AUTHENTICATION" + end + + self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, row_vertical_padding, 0, 0}, "") top_pane_alignment_ref = {"TOPLEFT", self.controls.sectionAnchor, "TOPLEFT"} local scrollBarShown = #slotTables > 21 -- clipping starts beyond this -- dynamically hide rows that are above or below the scrollBar @@ -508,6 +593,16 @@ Highest Weight - Displays the order retrieved from trade]] self.controls["name"..row_count].shown = function() return hideRowFunc(self, row_count) and self:findValidSlotForWatchersEye() end + + -- fix case where the row count is reduced from the last time the popup was + -- opened, which would leave extra row controls in the menu + for k, v in pairs(self.controls) do + local number = k:match("(%d+)") + if number and tonumber(number) > row_count then + self.controls[k] = nil + end + end + row_count = row_count + 1 local effective_row_count = row_count - ((scrollBarShown and #slotTables >= 19) and #slotTables-19 or 0) + 2 + 2 -- Two top menu rows, two bottom rows, slots after #19 overlap the other controls at the bottom of the pane @@ -530,13 +625,17 @@ Highest Weight - Displays the order retrieved from trade]] main:ClosePopup() -- there's a case where if you have a socket(s) allocated, open TradeQuery, close it, dealloc, then open TradeQuery again -- the deallocated socket controls were still showing, so this will remove all dynamically created controls from items - wipeItemControls() + + -- later note: this is disabled because it causes the trader to crash if + -- it's closed mid-search + -- wipeItemControls() end) self.controls.updateCurrencyConversion = new("ButtonControl", {"BOTTOMLEFT", nil, "BOTTOMLEFT"}, {pane_margins_horizontal, -pane_margins_vertical, 240, row_height}, "Get Currency Conversion Rates", function() self:PullPoENinjaCurrencyConversion(self.pbLeague) end) self.controls.pbNotice = new("LabelControl", {"BOTTOMRIGHT", nil, "BOTTOMRIGHT"}, {-row_height - pane_margins_vertical - row_vertical_padding, -pane_margins_vertical, 300, row_height}, "") + self:SetCurrencyConversionButton() -- used in PopupDialog:Draw() local function scrollBarFunc() @@ -791,10 +890,10 @@ function TradeQueryClass:UpdateControlsWithItems(row_idx) local sortMode = self.itemSortSelectionList[self.pbItemSortSelectionIndex] local sortedItems, errMsg = self:SortFetchResults(row_idx, sortMode) if errMsg == "MissingConversionRates" then - self:SetNotice(self.controls.pbNotice, "^4Price sorting is not available, falling back to Stat Value sort.") + self:SetNotice(self.controls.pbNotice, "^4Please update currency rates to sort by price. Falling back to Stat Value sort.") sortedItems, errMsg = self:SortFetchResults(row_idx, self.sortModes.StatValue) - end - if errMsg then + return + elseif errMsg then self:SetNotice(self.controls.pbNotice, "Error: " .. errMsg) return else @@ -837,19 +936,20 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) end return sum end + --- @return table? local function getPriceTable() - local out = {} - local pricedItems = self:addChaosEquivalentPriceToItems(self.resultTbl[row_idx]) - if pricedItems == nil then - return nil - end - for index, tbl in pairs(pricedItems) do - local chaosAmount = self:ConvertCurrencyToChaos(tbl.currency, tbl.amount) - if chaosAmount > 0 then - out[index] = chaosAmount - end + --- @type table + local divPrices = {} + for idx, item in ipairs(self.resultTbl[row_idx]) do + if item.currency and item.amount then + local divs = self:ConvertCurrencyToDivs(item.currency, item.amount) + if not divs then + return nil + end + divPrices[idx] = divs + else return nil end end - return out + return divPrices end local newTbl = {} if mode == self.sortModes.Weight then @@ -859,7 +959,6 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) return newTbl elseif mode == self.sortModes.StatValue then for result_index = 1, #self.resultTbl[row_idx] do - --ConPrintf("%.3f", getResultWeight(result_index)) t_insert(newTbl, { outputAttr = getResultWeight(result_index), index = result_index }) end table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end) @@ -879,7 +978,7 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) -- still seems to overrate very cheap items that are bad -- scaling factor for price - local k = 0.03 + local k = 0.1 t_insert(newTbl, { outputAttr = getResultWeight(result_index) - k * math.log(priceTable[result_index], 10), index = result_index }) @@ -900,19 +999,6 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) return newTbl end ---- Convert item prices to chaos equivalent using poeninja data, returns nil if fails to convert any -function TradeQueryClass:addChaosEquivalentPriceToItems(items) - local outputItems = copyTable(items) - for _, item in ipairs(outputItems) do - local chaosAmount = self:ConvertCurrencyToChaos(item.currency, item.amount) - if chaosAmount == nil then - return nil - end - item.chaosEquivalent = chaosAmount - end - return outputItems -end - -- return valid slot for Watcher's Eye -- Tries to first return an existing watcher's eye slot if possible function TradeQueryClass:findValidSlotForWatchersEye() @@ -935,7 +1021,6 @@ function TradeQueryClass:findValidSlotForWatchersEye() end end end - -- Method to generate pane elements for each item slot function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, row_vertical_padding, row_height) local controls = self.controls @@ -955,8 +1040,8 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro else self:SetNotice(context.controls.pbNotice, "") end - if main.POESESSID == nil or main.POESESSID == "" then - local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade/search", self.pbRealm, self.pbLeague) + if main.api.authToken == nil then + local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague) url = url .. "?q=" .. urlEncode(query) controls["uri"..context.row_idx]:SetText(url, true) return @@ -1006,7 +1091,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end) controls["bestButton"..row_idx].shown = function() return not self.resultTbl[row_idx] end controls["bestButton"..row_idx].enabled = function() return self.pbLeague end - controls["bestButton"..row_idx].tooltipText = "Creates a weighted search to find the highest Stat Value items for this slot." + controls["bestButton"..row_idx].tooltipText = [[Creates a weighted search to find the highest Stat Value items for this slot. +Note that even if you are authenticated, you can click this button again to show the search link. +If you have additional requirements that the trade tool doesn't cover (e.g. Adorned Magic jewels), +you can add them, copy the link here, and press "Price Item" to evaluate the items.]] local pbURL controls["uri"..row_idx] = new("EditControl", { "TOPLEFT", controls["bestButton"..row_idx], "TOPRIGHT"}, {8, 0, 514, row_height}, nil, nil, "^%C\t\n", nil, function(buf) local subpath = buf:match(self.hostName .. "trade/search/(.+)$") or "" @@ -1051,15 +1139,15 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end) end) controls["priceButton"..row_idx].enabled = function() - local poesessidAvailable = main.POESESSID and main.POESESSID ~= "" + local isAuthorized = main.api.authToken ~= nil local validURL = controls["uri"..row_idx].validURL local isSearching = controls["priceButton"..row_idx].label == "Searching..." - return poesessidAvailable and validURL and not isSearching + return isAuthorized and validURL and not isSearching end controls["priceButton"..row_idx].tooltipFunc = function(tooltip) tooltip:Clear() - if not main.POESESSID or main.POESESSID == "" then - tooltip:AddLine(16, "You must set your POESESSID to use search feature") + if not main.api.authToken then + tooltip:AddLine(16, "You must log in to use the search feature") elseif not controls["uri"..row_idx].validURL then tooltip:AddLine(16, "Enter a valid trade URL") end @@ -1162,7 +1250,9 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro local price = self.totalPrice[row_idx] and self.totalPrice[row_idx].amount .. " " .. self.totalPrice[row_idx].currency - if itemResult.whisper then + -- we also check the price type so we can prefer instant buyout over + -- whisper + if itemResult.whisper and (itemResult.priceType ~= "~b/o") then return price and "Whisper for " .. price or "Whisper" else return price and "Search for " .. price or "Search" @@ -1170,7 +1260,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end, function() local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] - if itemResult.whisper then + if itemResult.whisper and (itemResult.priceType ~= "~b/o") then Copy(itemResult.whisper) else local exactQuery = dkjson.decode(self.lastQueries[row_idx]) @@ -1186,13 +1276,11 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro exactQuery.query.filters.trade_filters.filters.account = { input = itemResult.trader } local exactQueryStr = dkjson.encode(exactQuery) - - self.tradeQueryRequests:SearchWithQuery(self.pbRealm, self.pbLeague, exactQueryStr, function(_, _) - end, {callbackQueryId = function(queryId) - local url = self.hostName.."trade/search/"..self.pbLeague.."/"..queryId - Copy(url) - OpenURL(url) - end}) + + local encodedUrl = s_format("https://www.pathofexile.com/trade/search/%s?q=%s", self.pbLeague, urlEncode(exactQueryStr)) + + Copy(encodedUrl) + OpenURL(encodedUrl) end end) @@ -1205,6 +1293,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro tooltip:AddLine(16, text) end end + -- Method to update the Total Price string sum of all items function TradeQueryClass:GetTotalPriceString() local text = "" @@ -1244,38 +1333,26 @@ function TradeQueryClass:UpdateRealms() self.controls.realm:SetSel(self.pbRealmIndex) end - if main.POESESSID and main.POESESSID ~= "" then - -- Fetch from trade page using POESESSID, includes private leagues - ConPrintf("Fetching realms and leagues using POESESSID") - self.tradeQueryRequests:FetchRealmsAndLeaguesHTML(function(data, errMsg) + -- use trade leagues api to get trade leagues including private leagues is valid. + for _, realmId in pairs (self.realmIds) do + self.tradeQueryRequests:FetchLeagues(realmId, function(leagues, errMsg) if errMsg then - self:SetNotice(self.controls.pbNotice, "Error while fetching league list: "..errMsg) - return + self:SetNotice(self.controls.pbNotice, "Using Fallback Error while fetching league list: "..errMsg) end - local leagues = data.leagues self.allLeagues = {} - for _, value in ipairs(leagues) do - if not self.allLeagues[value.realm] then self.allLeagues[value.realm] = {} end - t_insert(self.allLeagues[value.realm], value.id) - end - self.realmIds = {} - for _, value in pairs(data.realms) do - -- filter out only Path of Exile one realms, as poe2 is not supported yet - if value.text:match("PoE 1 ") then - self.realmIds[value.text:gsub("PoE 1 ", "")] = value.id - end + for _, league in ipairs(leagues) do + if not self.allLeagues[realmId] then self.allLeagues[realmId] = {} end + t_insert(self.allLeagues[realmId], league) end setRealmDropList() end) - else - -- Fallback to static list - ConPrintf("Using static realms list") - self.realmIds = { - ["PC"] = "pc", - ["PS4"] = "sony", - ["Xbox"] = "xbox", - } - setRealmDropList() end + + -- perform a generic search to make sure the authorization is valid. + self.tradeQueryRequests:PerformSearch("pc", "Standard", [[{"query":{"status":{"option":"online"},"stats":[{"type":"and","filters":[]}]},"sort":{"price":"asc"}}]], function(response, errMsg) + if errMsg then + self:SetNotice(self.controls.pbNotice, "Error: " .. tostring(errMsg)) + end + end) end diff --git a/src/Classes/TradeQueryRateLimiter.lua b/src/Classes/TradeQueryRateLimiter.lua index 42573d0f3b..08cf07afec 100644 --- a/src/Classes/TradeQueryRateLimiter.lua +++ b/src/Classes/TradeQueryRateLimiter.lua @@ -8,30 +8,30 @@ ---@class TradeQueryRateLimiter local TradeQueryRateLimiterClass = newClass("TradeQueryRateLimiter", function(self) -- policies_sample = { - -- -- label: policy - -- ["trade-search-request-limit"] = { - -- -- label: rule - -- ["Ip"] = { - -- ["state"] = { - -- ["60"] = {["timeout"] = 0, ["request"] = 1}, - -- ["300"] = {["timeout"] = 0, ["request"] = 1}, - -- ["10"] = {["timeout"] = 0, ["request"] = 1} - -- }, - -- ["limits"] = { - -- ["60"] = {["timeout"] = 120, ["request"] = 15}, - -- ["300"] = {["timeout"] = 1800, ["request"] = 60}, - -- ["10"] = {["timeout"] = 60, ["request"] = 8} - -- } - -- }, - -- ["Account"] = { - -- ["state"] = { - -- ["5"] = {["timeout"] = 0, ["request"] = 1} - -- }, - -- ["limits"] = { - -- ["5"] = {["timeout"] = 60, ["request"] = 3} - -- } - -- } - -- } + -- -- label: policy + -- ["trade-search-request-limit"] = { + -- -- label: rule + -- ["Ip"] = { + -- ["state"] = { + -- ["60"] = {["timeout"] = 0, ["request"] = 1}, + -- ["300"] = {["timeout"] = 0, ["request"] = 1}, + -- ["10"] = {["timeout"] = 0, ["request"] = 1} + -- }, + -- ["limits"] = { + -- ["60"] = {["timeout"] = 120, ["request"] = 15}, + -- ["300"] = {["timeout"] = 1800, ["request"] = 60}, + -- ["10"] = {["timeout"] = 60, ["request"] = 8} + -- } + -- }, + -- ["Account"] = { + -- ["state"] = { + -- ["5"] = {["timeout"] = 0, ["request"] = 1} + -- }, + -- ["limits"] = { + -- ["5"] = {["timeout"] = 60, ["request"] = 3} + -- } + -- } + -- } -- } self.policies = {} self.retryAfter = {} @@ -52,7 +52,9 @@ local TradeQueryRateLimiterClass = newClass("TradeQueryRateLimiter", function(se -- state shows more requests than expected (external requests) self.pendingRequests = { ["trade-search-request-limit"] = {}, - ["trade-fetch-request-limit"] = {} + ["trade-fetch-request-limit"] = {}, + ["character-list-request-limit"] = {}, + ["character-request-limit"] = {} } end) @@ -69,10 +71,10 @@ function TradeQueryRateLimiterClass:ParseHeader(headerString) return headers end -function TradeQueryRateLimiterClass:ParsePolicy(headerString) +function TradeQueryRateLimiterClass:ParsePolicy(headerString, policy) local policies = {} local headers = self:ParseHeader(headerString) - local policyName = headers["x-rate-limit-policy"] + local policyName = headers["x-rate-limit-policy"] or policy policies[policyName] = {} local retryAfter = headers["retry-after"] if retryAfter then @@ -107,8 +109,11 @@ function TradeQueryRateLimiterClass:ParsePolicy(headerString) return policies end -function TradeQueryRateLimiterClass:UpdateFromHeader(headerString) - local newPolicies = self:ParsePolicy(headerString) +function TradeQueryRateLimiterClass:UpdateFromHeader(headerString, policy) + local newPolicies = self:ParsePolicy(headerString, policy) + if not newPolicies then + return + end for policyKey, policyValue in pairs(newPolicies) do if self.requestHistory[policyKey] == nil then self.requestHistory[policyKey] = { timestamps = {} } diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 95f8b6a5ad..2b46a21f41 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -19,41 +19,50 @@ local TradeQueryRequestsClass = newClass("TradeQueryRequests", function(self, ra end) ---Main routine for processing request queue -function TradeQueryRequestsClass:ProcessQueue() +--- @param onRateLimit fun(integer)? +function TradeQueryRequestsClass:ProcessQueue(onRateLimit) for key, queue in pairs(self.requestQueue) do if #queue > 0 then local policy = self.rateLimiter:GetPolicyName(key) local now = os.time() local timeNext = self.rateLimiter:NextRequestTime(policy, now) + local timeLeft = timeNext - now + -- relay wait info to caller when actually waiting, and not just + -- getting a magic poe2 release date number + if onRateLimit and timeLeft > 1 and timeNext ~= 1956528000 then + onRateLimit(timeLeft) + end if not (queue[1].retryTime and now < queue[1].retryTime) then if now >= timeNext then local request = table.remove(queue, 1) local requestId = self.rateLimiter:InsertRequest(policy) local onComplete = function(response, errMsg) self.rateLimiter:FinishRequest(policy, requestId) - self.rateLimiter:UpdateFromHeader(response.header) + self.rateLimiter:UpdateFromHeader(response.header, policy) if response.header:match("HTTP/[%d%.]+ (%d+)") == "429" then + local retryAfter = response.header:match("Retry%-After:%s+(%d+)") + retryAfter = retryAfter and tonumber(retryAfter) or 0 request.attempts = (request.attempts or 0) + 1 - local backoff = m_min(2 ^ request.attempts, 60) + + local backoff = math.max(math.min(2 ^ request.attempts, 60), retryAfter) request.retryTime = os.time() + backoff table.insert(queue, 1, request) + -- optional callback with the backoff time when rate + -- limited to inform user + if onRateLimit then + onRateLimit(backoff) + end return end - -- if limit rules don't return account then the POESESSID is invalid. - if response.header:match("[xX]%-[rR]ate%-[lL]imit%-[rR]ules: (.-)\n"):match("Account") == nil and main.POESESSID ~= "" then - main.POESESSID = "" - if errMsg then - errMsg = errMsg .. "\nPOESESSID is invalid. Please Re-Log and reset" - else - errMsg = "POESESSID is invalid. Please Re-Log and reset" - end + if errMsg == "Response code: 401" and response.body:find("invalid_token") then + errMsg = errMsg .. "\nAuthorization is invalid. Please Re-Log and reset" + main.api:ResetDetails() end request.callback(response.body, errMsg, unpack(request.callbackParams or {})) end - -- self:SendRequest(request.url , onComplete, {body = request.body, poesessid = main.POESESSID}) local header = "Content-Type: application/json" - if main.POESESSID ~= "" then - header = header .. "\nCookie: POESESSID=" .. main.POESESSID + if main.api.authToken then + header = header .."\nAuthorization: Bearer "..main.api.authToken end launch:DownloadPage(request.url, onComplete, { header = header, @@ -190,6 +199,7 @@ end ---Perform search and run callback function on returned item hashes. ---Item info has to be fetched separately +---@param realm string ---@param league string ---@param query string ---@param callback fun(response:table, errMsg:string) @@ -214,17 +224,13 @@ function TradeQueryRequestsClass:PerformSearch(realm, league, query, callback) callback(response, errMsg) end if response.error.message:find("Logging in will increase this limit") then - if main.POESESSID ~= "" then - errMsg = "POESESSID is invalid. Please Re-Log and reset" - else - errMsg = "Session is invalid. Please add your POESESSID" - end + errMsg = "Authorization is invalid. Please Re-Log and reset" else -- Report unhandled error errMsg = "[ " .. response.error.code .. ": " .. response.error.message .. " ]" end else - ConPrintf("Found 0 results for " .. self.hostName .. "api/trade/search/" .. league .. "/" .. response.id) + ConPrintf("Found 0 results for %sapi/trade/search/%s/%s", self.hostName, league, response.id) errMsg = "No Matching Results Found" end return callback(response, errMsg) @@ -285,11 +291,7 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) table.insert(items, { amount = trade_entry.listing.price.amount, currency = trade_entry.listing.price.currency, - -- note: using these to travel to the hideout or for a - -- direct whisper is not allowed, even if they are provided - -- right here - -- hideout_token = trade_entry.listing.hideout_token, - -- whisper_token = trade_entry.listing.whisper_token, + priceType = trade_entry.listing.price.type, item_string = common.base64.decode(trade_entry.item.extended.text), whisper = trade_entry.listing.whisper, trader = trade_entry.listing.account.name, @@ -302,7 +304,7 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) }) end ----@param callback fun(items:table, errMsg:string, query:string) +---@param callback fun(items:table, errMsg:string, query: string?) function TradeQueryRequestsClass:SearchWithURL(url, callback) local subpath = url:match(self.hostName .. "trade/search/(.+)$") local paths = {} @@ -318,10 +320,24 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) end league = paths[#paths-1] queryId = paths[#paths] - self:FetchSearchQueryHTML(realm, league, queryId, function(query, errMsg) + self:FetchSearchQuery(realm, league, queryId, function(query, errMsg) if errMsg then return callback(nil, errMsg, nil) end + + -- update sorting on provided url to sort by weights. + local json_data = dkjson.decode(query) + if not json_data or json_data.error then + errMsg = json_data and json_data.error or "Failed to parse search query JSON" + end + if json_data.query.stats and json_data.query.stats[1] and json_data.query.stats[1].type == "weight" then + json_data.sort = {} + json_data.sort["statgroup.0"] = "desc" + else + json_data.sort = { price = "asc"} + end + query = dkjson.encode(json_data) + self:SearchWithQuery(realm, league, query, function(items, searchErrMsg) callback(items, searchErrMsg, query) end) @@ -349,117 +365,30 @@ function TradeQueryRequestsClass:FetchSearchQuery(realm, league, queryId, callba }) end ---- HTML parsing to circumvent extra API call for query fetching ---- queryId -> query fetching via Poe API call costs precious search requests ---- But the search page HTML also contains the query object and this request is not throttled ----@param queryId string ----@param callback fun(query:string, errMsg:string) ----@see TradeQueryRequests#FetchSearchQuery -function TradeQueryRequestsClass:FetchSearchQueryHTML(realm, league, queryId, callback) - if main.POESESSID == "" then - return callback(nil, "Please provide your POESESSID") - end - local header = "Cookie: POESESSID=" .. main.POESESSID - launch:DownloadPage(self:buildUrl(self.hostName .. "trade/search", realm, league, queryId), - function(response, errMsg) - if errMsg then - return callback(nil, errMsg) - end - -- check if response.header includes "Cache-Control: must-revalidate" which indicates an invalid session - if response.header:lower():match("cache%-control:.+must%-revalidate") then - return callback(nil, "Failed to get search query, check POESESSID") - end - -- full json state obj from HTML - local dataStr = response.body:match('require%(%["main"%].+ t%((.+)%);}%);}%);') - if not dataStr then - return callback(nil, "JSON object not found on the page.") - end - local data, _, err = dkjson.decode(dataStr) - if err then - return callback(nil, "Failed to parse JSON object. ".. err) - end - local query = { query = data.state } - if data.state.stats and data.state.stats[1] and data.state.stats[1].type == "weight" then - query.sort = {} - query.sort["statgroup.0"] = "desc" - else - query.sort = { price = "asc"} - end - query.query.status = { option = query.query.status} -- works either way? - local queryStr = dkjson.encode(query) - callback(queryStr, errMsg) - end, - {header = header}) -end - ---- Fetches the list of all available leagues using HTML parsing ---- This should get all leagues, including the ones that are not available through API ---- ---- example output: ---- result = { ---- leagues = [ ---- { ---- "id": "Sanctum", ---- "realm": "pc", ---- "text": "Sanctum" ---- }, ---- ], ---- realms = [ ---- { ---- "id": "sony", ---- "text": "PS4" ---- }, ---- ] ---- } ----@param callback fun(result:table, errMsg:string) -function TradeQueryRequestsClass:FetchRealmsAndLeaguesHTML(callback) - if main.POESESSID == "" then - return callback(nil, "Please provide your POESESSID") - end - local header = "Cookie: POESESSID=" .. main.POESESSID - launch:DownloadPage( - self.hostName .. "trade", - function(response, errMsg) - if errMsg then - return callback(nil, errMsg) - end - -- full json state obj from HTML - local dataStr = response.body:match('require%(%["main"%].+ t%((.+)%);}%);}%);') - if not dataStr then - return callback(nil, "JSON object not found on the page.") - end - local data, _, err = dkjson.decode(dataStr) - if err then - return callback(nil, "Failed to parse JSON object. ".. err) - end - callback({leagues = data.leagues, realms = data.realms}, errMsg) - end, - {header = header} - ) -end - ---- Fetches the list of all available leagues using poe API +--- Fetches the list of all available leagues using trade2 league API ---@param realm string ---@param callback fun(query:table, errMsg:string) function TradeQueryRequestsClass:FetchLeagues(realm, callback) + local header = "Authorization: Bearer ".. (main.api.authToken or "") launch:DownloadPage( - self.hostName .. "api/leagues?compact=1&realm=" .. realm, - function(response, errMsg) - if errMsg then - return callback(nil, errMsg) - end - local json_data = dkjson.decode(response.body) - if not json_data or json_data.error then - errMsg = json_data and json_data.error or "Failed to get leagues" - end - local leagues = {} - for _, value in pairs(json_data) do - if (not value.id:find("SSF") and not value.id:find("Solo")) then + self.hostName .. "api/trade/data/leagues", + function(response, errMsg) + if errMsg then + return callback({"Standard", "Hardcore"}, errMsg) + end + local json_data = dkjson.decode(response.body) + if not json_data or json_data.error then + errMsg = json_data and json_data.error or "Failed to parse trade leagues JSON" + end + local leagues = {} + for _, value in pairs(json_data.result) do + if value.realm == realm then table.insert(leagues, value.id) end end - callback(leagues, errMsg) - end + callback(leagues, errMsg) + end, + {header = header} ) end diff --git a/src/HeadlessWrapper.lua b/src/HeadlessWrapper.lua index 774f3ae214..c089a20274 100644 --- a/src/HeadlessWrapper.lua +++ b/src/HeadlessWrapper.lua @@ -3,6 +3,7 @@ -- It can be run using a standard lua interpreter, although LuaJIT is preferable + -- Callbacks local callbackTable = { } local mainObject @@ -209,11 +210,15 @@ function loadBuildFromXML(xmlText, name) mainObject.main:SetMode("BUILD", false, name or "", xmlText) runCallback("OnFrame") end -function loadBuildFromJSON(getItemsJSON, getPassiveSkillsJSON) +function loadBuildFromJSON(characterJSON) mainObject.main:SetMode("BUILD", false, "") runCallback("OnFrame") - local charData = build.importTab:ImportItemsAndSkills(getItemsJSON) - build.importTab:ImportPassiveTreeAndJewels(getPassiveSkillsJSON, charData) + -- characterJSON could, for example, be the response from the PoE API: + -- https://www.pathofexile.com/developer/docs/reference#characters-get + local dkjson = require "dkjson" + local input = dkjson.decode(characterJSON) + local charData = build.importTab:ImportItemsAndSkills(input) + build.importTab:ImportPassiveTreeAndJewels(input) -- You now have a build without a correct main skill selected, or any configuration options set -- Good luck! end diff --git a/src/LaunchServer.lua b/src/LaunchServer.lua new file mode 100644 index 0000000000..428bbf9270 --- /dev/null +++ b/src/LaunchServer.lua @@ -0,0 +1,220 @@ +-- Start a server +local url = ... +local luaSocket = require("socket") +local server = luaSocket.tcp4() +local function bindSocket() + local res, err + res, err = server:bind("localhost", 49082) or server:bind("localhost", 49083) or server:bind("localhost", 49084) + if not res then + server:close() + else + res, err = server:listen(1) + if not res then + server:close() + else + return server + end + end + return nil, err +end +assert(bindSocket()) +local host, port = server:getsockname() +ConPrintf("Server started on %s:%s", host, port) + +local redirect_uri = string.format( + "http://localhost:%d", port +) +ConPrintf("Redirect URI: %s", redirect_uri) +url = url .. string.format("&redirect_uri=%s", redirect_uri) + +local commonResponse = [[ +HTTP/1.1 200 OK +Content-Type: text/html + + + + + + + PoB - Authentication Complete + + + +
+
+]] + +local commonResponseEnd = [[ +
+
+ + +]] + +ConPrintf("Opening URL: %s", url) +OpenURL(url) + +--- Handle an incoming socket connection, to complete an OAuth redirect. +--- @param client table @The socket connection to handle, as returned by `server:accept()`. +--- @param attempt number @The number of attempts made to handle an incoming connection. This is used for logging +--- purposes, since spurious issues can be difficult to identify otherwise. +--- @return boolean shouldRetry @Whether we should wait for another connection. If false, we've successfully responded +--- to a HTTP request. Note that, for the purposes of this function, we don't care whether authorization was *granted*, +--- just that the process itself was completed and the user was redirected as intended. +--- @return string? code @The OAuth authorization code. This is exchanged for an access token and refresh token later. +--- @return string? state @The OAuth state string. This is a sentinel value used to ensure that a request hasn't been +--- forged. +function handleConnection(client, attempt) + local shouldRetry, code, state = true, nil, nil + + local request, err = client:receive("*l") + if err then + ConPrintf("Attempt %d to handle incoming connection failed: %s", attempt, err) + elseif request then + local response + local _, _, method, path, version = request:find("^(%S+)%s(%S+)%s(%S+)") + if method ~= "GET" then + ConPrintf( + "Attempt %d to handle incoming connection received an invalid HTTP request: non-GET method %s", + attempt, + method + ) + + return true + end + + local queryParams = {} + for k, v in path:gmatch("([^&=?]+)=([^&=?]+)") do + queryParams[k] = v:gsub("%%(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end) + end + + if queryParams["code"] ~= nil then + response = commonResponse .. [[ +

PoB - Authentication Successful

+

✅ Your authentication is complete! You can now return to the app.

+ ]] .. commonResponseEnd + code = queryParams["code"] + state = queryParams["state"] + else + response = commonResponse .. [[ +

PoB - Authentication Failed

+

❌ Authentication failed. Please try again.

+ ]] .. queryParams["error"] .. ": " .. queryParams["error_description"] .. [[ + ]] .. commonResponseEnd + end + + shouldRetry = false + if attempt ~= 1 then + ConPrintf("Attempt %d to handle incoming connection received a valid HTTP request", attempt) + end + + -- Send HTTP Response + --ConPrintf("Sending response: %s", response) + client:send(response) + end + + return shouldRetry, code, state +end + +-- Misbehaving software (think VPNs, anything network-related, even OS services) will occasionally attempt to connect +-- to newly-opened sockets for one reason or another. Previously, PoB only waited for one connection, and gave up +-- immediately if something went wrong. +-- +-- This would result in a sequence of events roughly like this: +-- 1. PoB opens a socket +-- 2. A misbehaving piece of software connects to the socket, sends nothing, then terminates the connection +-- 3. PoB tries to read from the socket, receives an error since the connection is terminated, and closes the server +-- 4. OAuth authorization succeeds, but by the time the user is redirected back to PoB, the server is already closed +-- 5. PoB never receives the OAuth redirect, and doesn't have any of the information necessary to use the API +-- +-- To avoid this, we instead allow for any number of incoming connections, and simply stop listening for them once +-- either a) 30 seconds have elapsed or b) we've received a legitimate HTTP request and responded to it. +-- +-- Unfortunately, this still isn't perfect: in theory, two applications (such as a browser, and something else) could +-- attempt to establish a connection at the same time. In the future, this could be refactored to perform non-blocking +-- IO, so that it can operate concurrently, but hopefully that isn't necessary. +local attempt = 1 +local stopAt = os.time() + 30 +local errMsg +local shouldRetry, code, state = true, nil, nil +while (os.time() < stopAt) and shouldRetry do + -- `settimeout`` applies only to individual operations, but we're more concerned with not spending more than 30 + -- seconds *total* waiting, so we adjust with each iteration as necessary. + local remainingTime = math.max(0, stopAt - os.time()) + server:settimeout(remainingTime) + + local client = server:accept() + if not client then + goto retry + end + + client:settimeout(5) + shouldRetry, code, state = handleConnection(client, attempt) + client:close() + + :: retry :: + attempt = attempt + 1 +end +server:close() +if os.time() >= stopAt then + errMsg = "Timeout reached without a response received by the local server" +end +return code, errMsg, state, port diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index 3f6e19d74d..fd76a6c6c9 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -114,13 +114,12 @@ function main:Init() self.migrateEldritchImplicits = true self.notSupportedModTooltips = true self.notSupportedTooltipText = " ^8(Not supported in PoB yet)" - self.POESESSID = "" self.showPublicBuilds = true self.showFlavourText = true self.showAnimations = true self.showAllItemAffixes = true self.errorReadingSettings = false - + if not SetDPIScaleOverridePercent then SetDPIScaleOverridePercent = function(scale) end end if launch.devMode and IsKeyDown("CTRL") or os.getenv("REGENERATE_MOD_CACHE") == "1" then @@ -524,6 +523,10 @@ function main:LoadSettings(ignoreBuild) elseif node.elem == "Accounts" then self.lastAccountName = node.attrib.lastAccountName self.lastRealm = node.attrib.lastRealm + self.lastLeague = node.attrib.lastLeague + self.lastToken = node.attrib.lastToken + self.lastRefreshToken = node.attrib.lastRefreshToken + self.tokenExpiry = tonumber(node.attrib.tokenExpiry) for _, child in ipairs(node) do if child.elem == "Account" then self.gameAccounts[child.attrib.accountName] = { @@ -605,9 +608,6 @@ function main:LoadSettings(ignoreBuild) if node.attrib.notSupportedModTooltips then self.notSupportedModTooltips = node.attrib.notSupportedModTooltips == "true" end - if node.attrib.POESESSID then - self.POESESSID = node.attrib.POESESSID or "" - end if node.attrib.invertSliderScrollDirection then self.invertSliderScrollDirection = node.attrib.invertSliderScrollDirection == "true" end @@ -714,7 +714,7 @@ function main:SaveSettings() return true end t_insert(setXML, mode) - local accounts = { elem = "Accounts", attrib = { lastAccountName = self.lastAccountName, lastRealm = self.lastRealm } } + local accounts = { elem = "Accounts", attrib = { lastAccountName = self.lastAccountName, lastRealm = self.lastRealm, lastLeague = self.lastLeague, lastToken = self.lastToken, lastRefreshToken = self.lastRefreshToken, tokenExpiry = tostring(self.tokenExpiry) } } for accountName, account in pairs(self.gameAccounts) do t_insert(accounts, { elem = "Account", attrib = { accountName = accountName, sessionID = account.sessionID } }) end @@ -754,7 +754,6 @@ function main:SaveSettings() slotOnlyTooltips = tostring(self.slotOnlyTooltips), migrateEldritchImplicits = tostring(self.migrateEldritchImplicits), notSupportedModTooltips = tostring(self.notSupportedModTooltips), - POESESSID = self.POESESSID, invertSliderScrollDirection = tostring(self.invertSliderScrollDirection), disableDevAutoSave = tostring(self.disableDevAutoSave), showPublicBuilds = tostring(self.showPublicBuilds), @@ -968,7 +967,7 @@ function main:OpenOptionsPopup() controls.showAnimations = new("CheckBoxControl", { "TOPLEFT", nil, "TOPLEFT" }, { defaultLabelPlacementX, currentY, 20 }, "^7Show Animations:", function(state) self.showAnimations = state end) - + nextRow() controls.showAllItemAffixes = new("CheckBoxControl", { "TOPLEFT", nil, "TOPLEFT" }, { defaultLabelPlacementX, currentY, 20 }, "^7Show all item affixes sliders:", function(state) self.showAllItemAffixes = state