-- luatex-core security and io overloads ..

-- if not modules then modules = { } end modules ['luatex-core'] = {
--     version   = 1.112,
--     comment   = 'companion to luatex',
--     author    = 'Hans Hagen & Luigi Scarso',
--     copyright = 'LuaTeX Development Team',
-- }

LUATEXCOREVERSION = 1.161 -- we reflect the luatex version where changes happened

-- This file overloads some Lua functions. The readline variants provide the same
-- functionality as LuaTeX <= 1.04 and doing it this way permits us to keep the
-- original io libraries clean. Performance is probably even a bit better now.

-- We test for functions already being defined so that we don't overload ones that
-- are provided in the startup script.

local saferoption = status.safer_option
local shellescape = status.shell_escape -- 0 (disabled) 1 (anything) 2 (restricted)
local kpseused    = status.kpse_used    -- 0 1

if kpseused == 1 then

    local type = type
    local gsub = string.gsub
    local find = string.find

    local mt                    = getmetatable(io.stderr)
    local mt_lines              = mt.lines

    local kpse_checkpermission  = kpse.check_permission
    local kpse_recordinputfile  = kpse.record_input_file
    local kpse_recordoutputfile = kpse.record_output_file

    local kpse_outputnameok     = kpse.out_name_ok
    local kpse_inputnameok      = kpse.in_name_ok

    local io_open               = io.open
    local io_popen              = kpse.popen or io.popen -- should have been be in the kpse namespace
    local io_lines              = io.lines

    local fio_readline          = fio.readline

    local write_nl              = texio.write_nl

    io.saved_lines              = io_lines -- always readonly
    mt.saved_lines              = mt_lines -- always readonly

    -- The version below is different from the luatex one (which is probably made for
    -- LaTeX) because read should not be the default (e.g. when how == 'q'). Also in
    -- that version record doesn't always work for write. How about append?

    local function validinput(name,how)
        if type(how) ~= "string" or how == "" then
            how = "r"
        end
        return not find(how,"w") and kpse_inputnameok(name) and io_open(name,how)
    end

    local function validoutput(name,how)
        return type(how) == "string" and find(how,"w") and kpse_outputnameok(name) and io_open(name,how)
    end

    local function luatex_io_open(name,how)
        local handle = validinput(name,how)
        if handle then
            kpse_recordinputfile(name,"r")
        else
            handle = validoutput(name,how)
            if handle then
                kpse_recordoutputfile(name,"w")
            end
        end
        return handle
    end

    local function luatex_io_open_readonly(name,how)
        local handle = validinput(name,how)
        if handle then
            kpse_recordinputfile(name,"r")
        end
        return handle
    end

 -- local function luatex_io_popen(name,...)
 --     local okay, found = kpse_checkpermission(name)
 --     if okay and found then
 --         return io_popen(found,...)
 --     end
 -- end

    -- For some reason the gc doesn't kick in so we need to close explicitly
    -- so that the handle is flushed.

    local error, type = error, type

    local function luatex_io_lines(name,how)
        if type(name) == "string" then
            local handle = validinput(name,how)
            if handle then
                kpse_recordinputfile(name,"r")
                return function()
                    local l = fio_readline(f)
                    if not l then
                        f:close()
                    end
                    return l
                end
            else
                -- for those who like it this way:
                error("patched 'io.lines' can't open '" .. name .. "'")
            end
        else
            return io_lines()
        end
    end

    local function luatex_io_readline(f)
        return function()
            return fio_readline(f)
        end
    end

    io.lines = luatex_io_lines
    mt.lines = luatex_io_readline

    io.open  = luatex_io_open
 -- io.popen = luatex_io_popen
    io.popen = io_popen

else

    -- we assume management elsewhere

end

-- maybe also only when in kpse mode

if saferoption == 1 then

    local write_nl = texio.write_nl
    local format   = string.format

    local function installdummy(str,f)
        local reported = false
        return function(...)
            if not reported then
                write_nl(format("safer option set, function %q is %s",
                    str,f and "limited" or "disabled"))
                reported = true
            end
            if f then
                return f(...)
            end
        end
    end

    local function installlimit(str,f)
        local reported = false
    end

    os.execute   = installdummy("os.execute")
    os.spawn     = installdummy("os.spawn")
    os.exec      = installdummy("os.exec")
    os.setenv    = installdummy("os.setenv")
    os.tempdir   = installdummy("os.tempdir")

    io.popen     = installdummy("io.popen")
    io.open      = installdummy("io.open",luatex_io_open_readonly)

    os.kpsepopen = io.popen -- because it's in the os namespace ... brr

    os.rename    = installdummy("os.rename")
    os.remove    = installdummy("os.remove")

    io.tmpfile   = installdummy("io.tmpfile")
    io.output    = installdummy("io.output")

    lfs.chdir    = installdummy("lfs.chdir")
    lfs.lock     = installdummy("lfs.lock")
    lfs.touch    = installdummy("lfs.touch")
    lfs.rmdir    = installdummy("lfs.rmdir")
    lfs.mkdir    = installdummy("lfs.mkdir")

    debug = nil
    package.loaded.debug = nil

    -- os.[execute|os.spawn|os.exec] already are shellescape aware)

end

-- maybe also only when in kpse mode

if saferoption == 1 or shellescape ~= 1 then

    package.loadlib      = function() end
    package.searchers[4] = nil
    package.searchers[3] = nil

    if os.setenv then
        os.setenv = function(...) end -- Great, this will fail for some usage.
    end

    ffi = require('ffi')

    if ffi then
        for k, v in next, ffi do
            if k ~= 'gc' then
                ffi[k] = nil
            end
        end
    end

    ffi = nil

    -- A patch by LS for LaTeX per April 2022 but I'm, not sure if that breaks ConTeXt MKIV. I have
    -- no time nor motivation to test that right now so we'll see where it breaks. We have different
    -- loaders anyway and we don't test plain.

    package.loaded .ffi = nil   -- Isn't that still nil then?
    package.preload.ffi = error -- Do errors always go there?

end

if md5 then

    local sum    = md5.sum
    local gsub   = string.gsub
    local format = string.format
    local byte   = string.byte

    if not md5.sumhexa then
        function md5.sumhexa(k)
            return (gsub(sum(k), ".", function(c)
                return format("%02x",byte(c))
            end))
        end
    end

    if not md5.sumHEXA then
        function md5.sumHEXA(k)
            return (gsub(sum(k), ".", function(c)
                return format("%02X",byte(c))
            end))
        end
    end

end

-- compatibility: this might go away

if not unpack then
    unpack = table.unpack
end

if not package.loaders then
    package.loaders = package.searchers
end

if not loadstring then
    loadstring = load
end

-- compatibility: this might stay

if bit32 then

    -- lua 5.2: we're okay

elseif utf8 then

    -- lua 5.3:  bitwise.lua, v 1.24 2014/12/26 17:20:53 roberto

    bit32 = load ( [[
local select = select -- instead of: arg = { ... }

bit32 = {
  bnot = function (a)
    return ~a & 0xFFFFFFFF
  end,
  band = function (x, y, z, ...)
    if not z then
      return ((x or -1) & (y or -1)) & 0xFFFFFFFF
    else
      local res = x & y & z
      for i=1,select("#",...) do
        res = res & select(i,...)
      end
      return res & 0xFFFFFFFF
    end
  end,
  bor = function (x, y, z, ...)
    if not z then
      return ((x or 0) | (y or 0)) & 0xFFFFFFFF
    else
      local res = x | y | z
      for i=1,select("#",...) do
        res = res | select(i,...)
      end
      return res & 0xFFFFFFFF
    end
  end,
  bxor = function (x, y, z, ...)
    if not z then
      return ((x or 0) ~ (y or 0)) & 0xFFFFFFFF
    else
      local res = x ~ y ~ z
      for i=1,select("#",...) do
        res = res ~ select(i,...)
      end
      return res & 0xFFFFFFFF
    end
  end,
  btest = function (x, y, z, ...)
    if not z then
      return (((x or -1) & (y or -1)) & 0xFFFFFFFF) ~= 0
    else
      local res = x & y & z
      for i=1,select("#",...) do
          res = res & select(i,...)
      end
      return (res & 0xFFFFFFFF) ~= 0
    end
  end,
  lshift = function (a, b)
    return ((a & 0xFFFFFFFF) << b) & 0xFFFFFFFF
  end,
  rshift = function (a, b)
    return ((a & 0xFFFFFFFF) >> b) & 0xFFFFFFFF
  end,
  arshift = function (a, b)
    a = a & 0xFFFFFFFF
    if b <= 0 or (a & 0x80000000) == 0 then
      return (a >> b) & 0xFFFFFFFF
    else
      return ((a >> b) | ~(0xFFFFFFFF >> b)) & 0xFFFFFFFF
    end
  end,
  lrotate = function (a ,b)
    b = b & 31
    a = a & 0xFFFFFFFF
    a = (a << b) | (a >> (32 - b))
    return a & 0xFFFFFFFF
  end,
  rrotate = function (a, b)
    b = -b & 31
    a = a & 0xFFFFFFFF
    a = (a << b) | (a >> (32 - b))
    return a & 0xFFFFFFFF
  end,
  extract = function (a, f, w)
    return (a >> f) & ~(-1 << (w or 1))
  end,
  replace = function (a, v, f, w)
    local mask = ~(-1 << (w or 1))
    return ((a & ~(mask << f)) | ((v & mask) << f)) & 0xFFFFFFFF
  end,
}
        ]] )

elseif bit then

    -- luajit (for now)

    bit32 = load ( [[
local band, bnot, rshift, lshift = bit.band, bit.bnot, bit.rshift, bit.lshift

bit32 = {
  arshift = bit.arshift,
  band    = band,
  bnot    = bnot,
  bor     = bit.bor,
  bxor    = bit.bxor,
  btest   = function(...)
    return band(...) ~= 0
  end,
  extract = function(a,f,w)
    return band(rshift(a,f),2^(w or 1)-1)
  end,
  lrotate = bit.rol,
  lshift  = lshift,
  replace = function(a,v,f,w)
    local mask = 2^(w or 1)-1
    return band(a,bnot(lshift(mask,f)))+lshift(band(v,mask),f)
  end,
  rrotate = bit.ror,
  rshift  = rshift,
}
        ]] )

else

    -- hope for the best or fail

    bit32 = require("bit32")

end

-- this is needed for getting require("socket") right

do

    local loaded = package.loaded

    if not loaded.socket then loaded.socket = loaded["socket.core"] end
    if not loaded.mime   then loaded.mime   = loaded["mime.core"]   end

    if not loaded.lfs then loaded.lfs = lfs end

end

do

    local lfsattributes     = lfs.attributes
    local symlinkattributes = lfs.symlinkattributes

    -- these can now be done using lfs (was dead slow before)

    if not lfs.isfile then
        function lfs.isfile(name)
            local m = lfsattributes(name,"mode")
            return m == "file" or m == "link"
        end
    end

    if not lfs.isdir then
        function lfs.isdir(name)
            local m = lfsattributes(name,"mode")
            return m == "directory"
        end
    end

    -- shortnames have also be sort of dropped from kpse

    if not lfs.shortname then
        function lfs.shortname(name)
            return name
        end
    end

    -- now there is a target field, so ...

    if not lfs.readlink then
        function lfs.readlink(name)
            return symlinkattributes(name,"target") or nil
        end
    end

end

-- start omit

if utilities and utilities.merger and utilities.merger.compact then

    local byte, format, gmatch, gsub = string.byte, string.format, string.gmatch, string.gsub
    local concat = table.concat

    local data = io.loaddata('luatex-core.lua')

    data = gsub(data,'%-%-%s*start%s*omit.-%-%-%s*stop%s*omit%s*','')
    data = gsub(data,'\r\n','\n')

    local t = { }
    local r = { }
    local n = 0
    local s = utilities.merger.compact(data) -- no comments and less spaces

    t[#t+1] = '/* generated from and by luatex-core.lua */'
    t[#t+1] = ''
    t[#t+1] = '#include "lua.h"'
    t[#t+1] = '#include "lauxlib.h"'
    t[#t+1] = ''
    t[#t+1] = 'int load_luatex_core_lua (lua_State * L);'
    t[#t+1] = ''
    t[#t+1] = 'int load_luatex_core_lua (lua_State * L)'
    t[#t+1] = '{'
    t[#t+1] = '  static unsigned char luatex_core_lua[] = {'
    for c in gmatch(data,'.') do
        if n == 16 then
            n = 1
            t[#t+1] = '    ' .. concat(r,', ') .. ','
        else
            n = n + 1
        end
        r[n] = format('0x%02x',byte(c))
    end
    n = n + 1
    r[n] = '0x00'
    t[#t+1] = '    ' .. concat(r,', ',1,n)
    t[#t+1] = '  };'
 -- t[#t+1] = format('unsigned int luatex_core_lua_len = 0x%x;',#d+1)
    t[#t+1] = '  return luaL_dostring(L, (const char*) luatex_core_lua);'
    t[#t+1] = '}'

    io.savedata('luatex-core.c',concat(t,'\n'))
    io.savedata('luatex-core-stripped.lua',s)

end

-- stop omit
