From 875c97e37b6cd92be40f7650851f440f99aa09ff Mon Sep 17 00:00:00 2001 From: Pablo Mayobre Date: Mon, 16 Feb 2015 19:10:38 -0300 Subject: [PATCH] One commit to rule them all One commit to fix them, One commit to pull them all and in Github merge them (https://github.com/insweater/gamejoltlua/pull/8/commits) --- gamejolt/http.lua | 350 ---------------------------------------------- gamejolt/init.lua | 93 +++++++++--- gamejolt/md5.lua | 293 +++++++++++++++++++------------------- 3 files changed, 223 insertions(+), 513 deletions(-) delete mode 100644 gamejolt/http.lua diff --git a/gamejolt/http.lua b/gamejolt/http.lua deleted file mode 100644 index ad8db1e..0000000 --- a/gamejolt/http.lua +++ /dev/null @@ -1,350 +0,0 @@ ------------------------------------------------------------------------------ --- HTTP/1.1 client support for the Lua language. --- LuaSocket toolkit. --- Author: Diego Nehab --- RCS ID: $Id: http.lua,v 1.71 2007/10/13 23:55:20 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies -------------------------------------------------------------------------------- -local socket = require("socket") -local url = require("socket.url") -local ltn12 = require("ltn12") -local mime = require("mime") -local string = require("string") -local base = _G -local table = require("table") -module("socket.http") - ------------------------------------------------------------------------------ --- Program constants ------------------------------------------------------------------------------ --- connection timeout in seconds -TIMEOUT = 60 --- default port for document retrieval -PORT = 80 --- user agent field sent in request -USERAGENT = socket._VERSION - ------------------------------------------------------------------------------ --- Reads MIME headers from a connection, unfolding where needed ------------------------------------------------------------------------------ -local function receiveheaders(sock, headers) - local line, name, value, err - headers = headers or {} - -- get first line - line, err = sock:receive() - if err then return nil, err end - -- headers go until a blank line is found - while line ~= "" do - -- get field-name and value - name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) - if not (name and value) then return nil, "malformed reponse headers" end - name = string.lower(name) - -- get next line (value might be folded) - line, err = sock:receive() - if err then return nil, err end - -- unfold any folded values - while string.find(line, "^%s") do - value = value .. line - line = sock:receive() - if err then return nil, err end - end - -- save pair in table - if headers[name] then headers[name] = headers[name] .. ", " .. value - else headers[name] = value end - end - return headers -end - ------------------------------------------------------------------------------ --- Extra sources and sinks ------------------------------------------------------------------------------ -socket.sourcet["http-chunked"] = function(sock, headers) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - -- get chunk size, skip extention - local line, err = sock:receive() - if err then return nil, err end - local size = base.tonumber(string.gsub(line, ";.*", ""), 16) - if not size then return nil, "invalid chunk size" end - -- was it the last chunk? - if size > 0 then - -- if not, get chunk and skip terminating CRLF - local chunk, err, part = sock:receive(size) - if chunk then sock:receive() end - return chunk, err - else - -- if it was, read trailers into headers table - headers, err = receiveheaders(sock, headers) - if not headers then return nil, err end - end - end - }) -end - -socket.sinkt["http-chunked"] = 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 return sock:send("0\r\n\r\n") end - local size = string.format("%X\r\n", string.len(chunk)) - return sock:send(size .. chunk .. "\r\n") - end - }) -end - ------------------------------------------------------------------------------ --- Low level HTTP API ------------------------------------------------------------------------------ -local metat = { __index = {} } - -function open(host, port, create) - -- create socket with user connect function, or with default - local c = socket.try((create or socket.tcp)()) - local h = base.setmetatable({ c = c }, metat) - -- create finalized try - h.try = socket.newtry(function() h:close() end) - -- set timeout before connecting - h.try(c:settimeout(TIMEOUT)) - h.try(c:connect(host, port or PORT)) - -- here everything worked - return h -end - -function metat.__index:sendrequestline(method, uri) - local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) - return self.try(self.c:send(reqline)) -end - -function metat.__index:sendheaders(headers) - local h = "\r\n" - for i, v in base.pairs(headers) do - h = i .. ": " .. v .. "\r\n" .. h - end - self.try(self.c:send(h)) - return 1 -end - -function metat.__index:sendbody(headers, source, step) - source = source or ltn12.source.empty() - step = step or ltn12.pump.step - -- if we don't know the size in advance, send chunked and hope for the best - local mode = "http-chunked" - if headers["content-length"] then mode = "keep-open" end - return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) -end - -function metat.__index:receivestatusline() - local status = self.try(self.c:receive(5)) - -- identify HTTP/0.9 responses, which do not contain a status line - -- this is just a heuristic, but is what the RFC recommends - if status ~= "HTTP/" then return nil, status end - -- otherwise proceed reading a status line - status = self.try(self.c:receive("*l", status)) - local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) - return self.try(base.tonumber(code), status) -end - -function metat.__index:receiveheaders() - return self.try(receiveheaders(self.c)) -end - -function metat.__index:receivebody(headers, sink, step) - sink = sink or ltn12.sink.null() - step = step or ltn12.pump.step - local length = base.tonumber(headers["content-length"]) - local t = headers["transfer-encoding"] -- shortcut - local mode = "default" -- connection close - if t and t ~= "identity" then mode = "http-chunked" - elseif base.tonumber(headers["content-length"]) then mode = "by-length" end - return self.try(ltn12.pump.all(socket.source(mode, self.c, length), - sink, step)) -end - -function metat.__index:receive09body(status, sink, step) - local source = ltn12.source.rewind(socket.source("until-closed", self.c)) - source(status) - return self.try(ltn12.pump.all(source, sink, step)) -end - -function metat.__index:close() - return self.c:close() -end - ------------------------------------------------------------------------------ --- High level HTTP API ------------------------------------------------------------------------------ -local function adjusturi(reqt) - local u = reqt - -- if there is a proxy, we need the full url. otherwise, just a part. - if not reqt.proxy and not PROXY then - u = { - path = socket.try(reqt.path, "invalid path 'nil'"), - params = reqt.params, - query = reqt.query, - fragment = reqt.fragment - } - end - return url.build(u) -end - -local function adjustproxy(reqt) - local proxy = reqt.proxy or PROXY - if proxy then - proxy = url.parse(proxy) - return proxy.host, proxy.port or 3128 - else - return reqt.host, reqt.port - end -end - -local function adjustheaders(reqt) - -- default headers - local lower = { - ["user-agent"] = USERAGENT, - ["host"] = reqt.host, - ["connection"] = "close, TE", - ["te"] = "trailers" - } - -- if we have authentication information, pass it along - if reqt.user and reqt.password then - lower["authorization"] = - "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) - end - -- override with user headers - for i,v in base.pairs(reqt.headers or lower) do - lower[string.lower(i)] = v - end - return lower -end - --- default url parts -local default = { - host = "", - port = PORT, - path ="/", - scheme = "http" -} - -local function adjustrequest(reqt) - -- parse url if provided - local nreqt = reqt.url and url.parse(reqt.url, default) or {} - -- explicit components override url - for i,v in base.pairs(reqt) do nreqt[i] = v end - if nreqt.port == "" then nreqt.port = 80 end - socket.try(nreqt.host and nreqt.host ~= "", - "invalid host '" .. base.tostring(nreqt.host) .. "'") - -- compute uri if user hasn't overriden - nreqt.uri = reqt.uri or adjusturi(nreqt) - -- ajust host and port if there is a proxy - nreqt.host, nreqt.port = adjustproxy(nreqt) - -- adjust headers in request - nreqt.headers = adjustheaders(nreqt) - return nreqt -end - -local function shouldredirect(reqt, code, headers) - return headers.location and - string.gsub(headers.location, "%s", "") ~= "" and - (reqt.redirect ~= false) and - (code == 301 or code == 302) and - (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") - and (not reqt.nredirects or reqt.nredirects < 5) -end - -local function shouldreceivebody(reqt, code) - if reqt.method == "HEAD" then return nil end - if code == 204 or code == 304 then return nil end - if code >= 100 and code < 200 then return nil end - return 1 -end - --- forward declarations -local trequest, tredirect - -function tredirect(reqt, location) - local result, code, headers, status = trequest { - -- the RFC says the redirect URL has to be absolute, but some - -- servers do not respect that - url = url.absolute(reqt.url, location), - source = reqt.source, - sink = reqt.sink, - headers = reqt.headers, - proxy = reqt.proxy, - nredirects = (reqt.nredirects or 0) + 1, - create = reqt.create - } - -- pass location header back as a hint we redirected - headers = headers or {} - headers.location = headers.location or location - return result, code, headers, status -end - -function trequest(reqt) - -- we loop until we get what we want, or - -- until we are sure there is no way to get it - local nreqt = adjustrequest(reqt) - local h = open(nreqt.host, nreqt.port, nreqt.create) - -- send request line and headers - h:sendrequestline(nreqt.method, nreqt.uri) - h:sendheaders(nreqt.headers) - -- if there is a body, send it - if nreqt.source then - h:sendbody(nreqt.headers, nreqt.source, nreqt.step) - end - local code, status = h:receivestatusline() - -- if it is an HTTP/0.9 server, simply get the body and we are done - if not code then - h:receive09body(status, nreqt.sink, nreqt.step) - return 1, 200 - end - local headers - -- ignore any 100-continue messages - while code == 100 do - headers = h:receiveheaders() - code, status = h:receivestatusline() - end - headers = h:receiveheaders() - -- at this point we should have a honest reply from the server - -- we can't redirect if we already used the source, so we report the error - if shouldredirect(nreqt, code, headers) and not nreqt.source then - h:close() - return tredirect(reqt, headers.location) - end - -- here we are finally done - if shouldreceivebody(nreqt, code) then - h:receivebody(headers, nreqt.sink, nreqt.step) - end - h:close() - return 1, code, headers, status -end - -local function srequest(u, b) - local t = {} - local reqt = { - url = u, - sink = ltn12.sink.table(t) - } - if b then - reqt.source = ltn12.source.string(b) - reqt.headers = { - ["content-length"] = string.len(b), - ["content-type"] = "application/x-www-form-urlencoded" - } - reqt.method = "POST" - end - local code, headers, status = socket.skip(1, trequest(reqt)) - return table.concat(t), code, headers, status -end - -request = socket.protect(function(reqt, body) - if base.type(reqt) == "string" then return srequest(reqt, body) - else return trequest(reqt) end -end) diff --git a/gamejolt/init.lua b/gamejolt/init.lua index 22787ec..b51bfef 100644 --- a/gamejolt/init.lua +++ b/gamejolt/init.lua @@ -1,16 +1,23 @@ -local folder = (...):gsub('%.init$', '') +local folder = ({...})[1]:gsub('%.init$', '') local md5 = require(folder .. ".md5" ) local http = require("socket.http") local GJ = { gameID, gameKey, isLoggedIn = false, - username, userToken + username, userToken, + trophies = {} } local BASE_URL = "http://gamejolt.com/api/game/v1/" -local function req(s, f, pu, pt) +local escape = function (a) + return tostring(a):gsub("([^%w%-%.%_])",function (a) + return string.format("%%%02X",string.byte(a)) + end) +end + +local function req(s, f, pu, pt, data) local url = BASE_URL .. s .. "&game_id=" .. tostring(GJ.gameID) .. "&format=" .. f if pu then url = url .. "&username=" .. GJ.username end if pt then url = url .. "&user_token=" .. GJ.userToken end @@ -18,7 +25,7 @@ local function req(s, f, pu, pt) local b = md5.sumhexa(url .. GJ.gameKey) url = url .. "&signature=" .. b - local r, e = http.request(url) + local r, e = http.request(url, data) return r end @@ -54,15 +61,46 @@ local function handleTrophies(str) return t end -function GJ.init(id, key) +function GJ.init(id, key, args) GJ.gameID = id GJ.gameKey = key + + if args and type(args)=="table" then + for k,v in pairs(args) do + local a = v:match("^gjapi_(.*)") + + if a then + key, value = a:match("^(.-)=(.-)$") + + if key == "username" then + GJ.username = value + elseif key == "token" then + GJ.userToken = value + end + end + end + end +end + +function GJ.getCredentials(dir) + local f = io.open(dir.."gjapi-credentials.txt") + + if f then + GJ.username = f:read() + GJ.userToken = f:read() + end + + if GJ.username and GJ.userToken then + return true, GJ.username, GJ.userToken + else + return false, "Couldn't find, open or read the \"gjapi-credentials.txt\" file" + end end -- users function GJ.authUser(name, token) - GJ.username = name - GJ.userToken = token + GJ.username = name or GJ.username or "" + GJ.userToken = token or GJ.userToken or "" local s = string.find(req("users/auth/?", "dump", true, true), "SUCCESS") ~= nil GJ.isLoggedIn = s @@ -112,7 +150,8 @@ function GJ.fetchData(key, isGlobal) local pu, pt = true, true if isGlobal then pu, pt = false, false end - local d = req("data-store/?key=" .. key, "dump", pu, pt) + local d = req("data-store/?key=" .. escape(key), "dump", pu, pt) + return string.sub(d, string.find(d, "\n"), string.len(d)) end @@ -120,14 +159,21 @@ function GJ.setData(key, data, isGlobal) local pu, pt = true, true if isGlobal then pu, pt = false, false end - return string.find(req("data-store/set/?key=" .. key .. "&data=" .. tostring(data), "dump", pu, pt), "SUCCESS") ~= nil + return string.find(req("data-store/set/?key=" .. escape(key) .. '&data=' .. escape(data), "dump", pu, pt), "SUCCESS") ~= nil +end + +function GJ.setBigData(key, data, isGlobal) + local pu, pt = true, true + if isGlobal then pu, pt = false, false end + + return string.find(req("data-store/set/?key=" .. escape(key), "dump", pu, pt, "data="..escape(data)), "SUCCESS") ~= nil end function GJ.updateData(key, value, operation, isGlobal) local pu, pt = true, true if isGlobal then pu, pt = false, false end - local d = req("data-store/update/?key=" .. key .. "&operation=" .. operation .. "&value=" .. tostring(value), "dump", pu, pt) + local d = req("data-store/update/?key=" .. escape(key) .. "&operation=" .. operation .. "&value=" .. escape(value), "dump", pu, pt) return string.sub(d, string.find(d, "\n"), string.len(d)) end @@ -135,7 +181,7 @@ function GJ.removeData(key, isGlobal) local pu, pt = true, true if isGlobal then pu, pt = false, false end - return string.find(req("data-store/remove/?key=" .. key, "dump", pu, pt), "SUCCESS") ~= nil + return string.find(req("data-store/remove/?key=" .. escape(key), "dump", pu, pt), "SUCCESS") ~= nil end function GJ.fetchStorageKeys(isGlobal) @@ -154,7 +200,9 @@ end -- trophies function GJ.giveTrophy(id) - return string.find(req("trophies/add-achieved/?trophy_id=" .. tostring(id), "dump", true, true), "SUCCESS") ~= nil + local s = string.find(req("trophies/add-achieved/?trophy_id=" .. id, "dump", true, true), "SUCCESS") ~= nil + GJ.fetchAllTrophies(true) + return end function GJ.fetchTrophy(id) @@ -168,29 +216,32 @@ function GJ.fetchTrophy(id) end function GJ.fetchTrophiesByStatus(achieved) - return handleTrophies("achieved=" .. tostring(achieved)) + return handleTrophies("achieved=" .. (achieved and "true" or "false")) end -function GJ.fetchAllTrophies() - return handleTrophies("") +function GJ.fetchAllTrophies(f) + if f then + GJ.trophies = handleTrophies("") + end + return GJ.trophies end -- scores function GJ.addScore(score, desc, tableID, guestName, extraData) local pu, pt, s = true, true, "" - if guestName then pu, pt = false, false, s .. "&guest=" .. guestName end + if guestName then pu, pt = false, false, s .. "&guest=" .. escape(guestName) end - if extraData then s = s .. "&extra_data=" .. tostring(extraData) end - if tableID then s = s .. "&table_id=" .. tostring(tableID) end + if extraData then s = s .. "&extra_data=" .. escape(extraData) end + if tableID then s = s .. "&table_id=" .. escape(tableID) end - return string.find(req("scores/add/?score=" .. tostring(desc) .. "&sort=" .. score .. s, "dump", pu, pt), "SUCCESS") ~= nil + return string.find(req("scores/add/?score=" .. escape(desc) .. "&sort=" .. score .. s, "dump", pu, pt), "SUCCESS") ~= nil end function GJ.fetchScores(limit, tableID) local pu, pt, s = true, true, "" - if tableID then pu, pt, s = false, false, "&table_id=" .. tostring(tableID) end + if tableID then pu, pt, s = false, false, "&table_id=" .. escape(tableID) end - local d = req("scores/?limit=" .. tostring(limit) .. s, "keypair", pu, pt) + local d = req("scores/?limit=" .. (tonumber(limit or "") or 10) .. s, "keypair", pu, pt) local t, f = {} parseKeypair(d, function(k, v) diff --git a/gamejolt/md5.lua b/gamejolt/md5.lua index 8b03439..a68bbc3 100644 --- a/gamejolt/md5.lua +++ b/gamejolt/md5.lua @@ -1,6 +1,6 @@ local md5 = { - _VERSION = "md5.lua 0.5.0", - _DESCRIPTION = "MD5 computation in Lua (5.1)", + _VERSION = "md5.lua 1.0.0", + _DESCRIPTION = "MD5 computation in Lua (5.1-3, LuaJIT)", _URL = "https://github.com/kikito/md5.lua", _LICENSE = [[ MIT LICENSE @@ -33,166 +33,175 @@ local md5 = { local floor, abs, max = math.floor, math.abs, math.max local char, byte, format, rep, sub = string.char, string.byte, string.format, string.rep, string.sub +local bit_or, bit_and, bit_not, bit_xor, bit_rshift, bit_lshift -local function check_int(n) - -- checking not float - if(n - floor(n) > 0) then - error("trying to use bitwise operation on non-integer!") - end -end +local ok, bit = pcall(require, 'bit') +if not ok then ok, bit = pcall(require, 'bit32') end -local function tbl2number(tbl) - local n = #tbl - - local rslt = 0 - local power = 1 - for i = 1, n do - rslt = rslt + tbl[i]*power - power = power*2 - end - - return rslt -end - -local function expand(tbl_m, tbl_n) - local big = {} - local small = {} - if(#tbl_m > #tbl_n) then - big = tbl_m - small = tbl_n - else - big = tbl_n - small = tbl_m - end - -- expand small - for i = #small + 1, #big do - small[i] = 0 - end - -end - -local to_bits -- needs to be declared before bit_not - -local function bit_not(n) - local tbl = to_bits(n) - local size = max(#tbl, 32) - for i = 1, size do - if(tbl[i] == 1) then - tbl[i] = 0 - else - tbl[i] = 1 - end - end - return tbl2number(tbl) -end - --- defined as local above -to_bits = function (n) - check_int(n) - if(n < 0) then - -- negative - return to_bits(bit_not(abs(n)) + 1) - end - -- to bits table - local tbl = {} - local cnt = 1 - while (n > 0) do - local last = math.mod(n,2) - if(last == 1) then - tbl[cnt] = 1 - else - tbl[cnt] = 0 - end - n = (n-last)/2 - cnt = cnt + 1 - end - - return tbl -end - -local function bit_or(m, n) - local tbl_m = to_bits(m) - local tbl_n = to_bits(n) - expand(tbl_m, tbl_n) - - local tbl = {} - local rslt = max(#tbl_m, #tbl_n) - for i = 1, rslt do - if(tbl_m[i]== 0 and tbl_n[i] == 0) then - tbl[i] = 0 - else - tbl[i] = 1 +if ok then + bit_or, bit_and, bit_not, bit_xor = bit.bor, bit.band, bit.bnot, bit.bxor + bit_rshift, bit_lshift = bit.rshift, bit.lshift +else + local function check_int(n) + -- checking not float + if(n - floor(n) > 0) then + error("trying to use bitwise operation on non-integer!") end end - return tbl2number(tbl) -end + local function tbl2number(tbl) + local n = #tbl -local function bit_and(m, n) - local tbl_m = to_bits(m) - local tbl_n = to_bits(n) - expand(tbl_m, tbl_n) - - local tbl = {} - local rslt = max(#tbl_m, #tbl_n) - for i = 1, rslt do - if(tbl_m[i]== 0 or tbl_n[i] == 0) then - tbl[i] = 0 - else - tbl[i] = 1 + local rslt = 0 + local power = 1 + for i = 1, n do + rslt = rslt + tbl[i]*power + power = power*2 end + + return rslt end - return tbl2number(tbl) -end - -local function bit_xor(m, n) - local tbl_m = to_bits(m) - local tbl_n = to_bits(n) - expand(tbl_m, tbl_n) - - local tbl = {} - local rslt = max(#tbl_m, #tbl_n) - for i = 1, rslt do - if(tbl_m[i] ~= tbl_n[i]) then - tbl[i] = 1 + local function expand(tbl_m, tbl_n) + local big = {} + local small = {} + if(#tbl_m > #tbl_n) then + big = tbl_m + small = tbl_n else - tbl[i] = 0 + big = tbl_n + small = tbl_m end + -- expand small + for i = #small + 1, #big do + small[i] = 0 + end + end - return tbl2number(tbl) -end + local to_bits -- needs to be declared before bit_not -local function bit_rshift(n, bits) - check_int(n) - - local high_bit = 0 - if(n < 0) then - -- negative - n = bit_not(abs(n)) + 1 - high_bit = 2147483648 -- 0x80000000 + function bit_not(n) + local tbl = to_bits(n) + local size = max(#tbl, 32) + for i = 1, size do + if(tbl[i] == 1) then + tbl[i] = 0 + else + tbl[i] = 1 + end + end + return tbl2number(tbl) end - for i=1, bits do - n = n/2 - n = bit_or(floor(n), high_bit) - end - return floor(n) -end + -- defined as local above + to_bits = function (n) + check_int(n) + if(n < 0) then + -- negative + return to_bits(bit_not(abs(n)) + 1) + end + -- to bits table + local tbl = {} + local cnt = 1 + while (n > 0) do + local last = math.mod(n,2) + if(last == 1) then + tbl[cnt] = 1 + else + tbl[cnt] = 0 + end + n = (n-last)/2 + cnt = cnt + 1 + end -local function bit_lshift(n, bits) - check_int(n) - - if(n < 0) then - -- negative - n = bit_not(abs(n)) + 1 + return tbl end - for i=1, bits do - n = n*2 + function bit_or(m, n) + local tbl_m = to_bits(m) + local tbl_n = to_bits(n) + expand(tbl_m, tbl_n) + + local tbl = {} + local rslt = max(#tbl_m, #tbl_n) + for i = 1, rslt do + if(tbl_m[i]== 0 and tbl_n[i] == 0) then + tbl[i] = 0 + else + tbl[i] = 1 + end + end + + return tbl2number(tbl) + end + + function bit_and(m, n) + local tbl_m = to_bits(m) + local tbl_n = to_bits(n) + expand(tbl_m, tbl_n) + + local tbl = {} + local rslt = max(#tbl_m, #tbl_n) + for i = 1, rslt do + if(tbl_m[i]== 0 or tbl_n[i] == 0) then + tbl[i] = 0 + else + tbl[i] = 1 + end + end + + return tbl2number(tbl) + end + + function bit_xor(m, n) + local tbl_m = to_bits(m) + local tbl_n = to_bits(n) + expand(tbl_m, tbl_n) + + local tbl = {} + local rslt = max(#tbl_m, #tbl_n) + for i = 1, rslt do + if(tbl_m[i] ~= tbl_n[i]) then + tbl[i] = 1 + else + tbl[i] = 0 + end + end + + return tbl2number(tbl) + end + + function bit_rshift(n, bits) + check_int(n) + + local high_bit = 0 + if(n < 0) then + -- negative + n = bit_not(abs(n)) + 1 + high_bit = 2147483648 -- 0x80000000 + end + + for i=1, bits do + n = n/2 + n = bit_or(floor(n), high_bit) + end + return floor(n) + end + + function bit_lshift(n, bits) + check_int(n) + + if(n < 0) then + -- negative + n = bit_not(abs(n)) + 1 + end + + for i=1, bits do + n = n*2 + end + return bit_and(n, 4294967295) -- 0xFFFFFFFF end - return bit_and(n, 4294967295) -- 0xFFFFFFFF end -- convert little-endian 32-bit int to a 4-char string