#!/usr/bin/luajit

-- version 0.8
-- https://aur.archlinux.org/packages/check-pacman-mtree/

--require "strict"

local ffi = require "ffi"

local C = ffi.C
local libcrypto = ffi.load("crypto")
local libz = ffi.load("z")

local function hex_from_ffi_str(s,n)
        return string.format(string.rep('%02x',n), string.byte(ffi.string(s,n),1,n))
end

local function C_error(str)
      return string.format(str .. ': %s', ffi.string(C.strerror(ffi.errno())))
end


ffi.cdef[[
   static const int LINE_SIZE = 1024;
   static const int BUFFER_SIZE = 32768;

   //libc start
   static const int O_RDONLY = 0;
   static const int SEEK_SET = 0;
   static const int SEEK_END = 2;
   static const int ENOENT = 2;
   char *strerror(int);
   int open (const char *, int, ...);
   int read(int , void *, int);
   int close(int);
   int lseek(int , int , int );
   int readlink(const char *, char *,int);

   //libc stop

   //openssl start
   static const int EVP_MAX_MD_SIZE = 64;
   void *EVP_md5(void);
   void *EVP_sha256(void);
   void *EVP_MD_CTX_new(void);
   int EVP_DigestInit(void *, void *);
   int EVP_DigestUpdate(void *,const void *,int);
   int EVP_DigestFinal_ex(void *,unsigned char *,unsigned int *);
   void EVP_MD_CTX_free(void *);
   //openssl stop

   //zlib start
   void * gzopen (const char *, const char *);
   int gzclose (void * );
   char * gzgets (void * , char *, int );
   //zlib stop
]]

local function printf_(...)
   return io.write(string.format(...),"\n")
end

-- if size matches we will not do md5/sha256
-- set size to -1 to force MD
local function get_size_md5_sha256(file,size)

   local fh,mdctx_md5,mdctx_sha256
   local fs_size,ret_2,ret_3

   do

      fh = C.open(file,C.O_RDONLY)
      if fh == -1 then return nil,C_error("open " .. file),ffi.errno() end

      fs_size = C.lseek(fh,0,C.SEEK_END)
   
      if size ~= -1 and fs_size ~= size then goto clean_up_short end
      --goto clean_up_short
   
   
      C.lseek(fh,0,C.SEEK_SET)
   
      mdctx_md5 = libcrypto.EVP_MD_CTX_new()
      mdctx_sha256 = libcrypto.EVP_MD_CTX_new()
   
      if mdctx_md5 == nil or mdctx_sha256 == nil then
            ret_2 = "libcrypto.EVP_MD_CTX_new md5/sha256 failed " .. file
            goto clean_up
      end
   
      local md_value_md5 = ffi.new("char[?]",C.EVP_MAX_MD_SIZE)
      local md_value_sha256 = ffi.new("char[?]",C.EVP_MAX_MD_SIZE)
   
      local md_len_md5 = ffi.new("int[1]")
      local md_len_sha256 = ffi.new("int[1]")
   
      local buffer = ffi.new("char[?]",C.BUFFER_SIZE)
   
      if md_value_md5 == nil or md_value_sha256 == nil or md_len_md5 == nil or
         md_len_sha256 == nil or buffer == nil then
         ret_2 = "ffi.new failed " ..file
         goto clean_up
      end
   
      if libcrypto.EVP_DigestInit(mdctx_md5, libcrypto.EVP_md5()) ~= 1 or
         libcrypto.EVP_DigestInit(mdctx_sha256, libcrypto.EVP_sha256()) ~= 1 then
            ret_2 = "libcrypto.EVP_DigestInit md5/sha256 failed " .. file
            goto clean_up
      end
   
      while (1) do
         local n = C.read(fh,buffer, C.BUFFER_SIZE)
         --we should make something better for read ret < 0
         if n <= 0 then break end
         if libcrypto.EVP_DigestUpdate(mdctx_md5,buffer,n) ~= 1 or
            libcrypto.EVP_DigestUpdate(mdctx_sha256,buffer,n) ~= 1 then
               ret_2 = "libcrypto.EVP_DigestUpdate md5/sha256 failed " .. file
               goto clean_up
         end
      end
   
      if libcrypto.EVP_DigestFinal_ex(mdctx_md5, md_value_md5, md_len_md5) ~= 1 or
         libcrypto.EVP_DigestFinal_ex(mdctx_sha256, md_value_sha256, md_len_sha256) ~= 1 then
           ret_2 = "libcrypto.EVP_DigestFinal_ex md5/sha256 failed " .. file
           goto clean_up
      end
   
      ret_2,ret_3 = hex_from_ffi_str(md_value_md5,md_len_md5[0]),hex_from_ffi_str(md_value_sha256,md_len_sha256[0])

   end

   ::clean_up::
      if mdctx_md5 ~= nil then libcrypto.EVP_MD_CTX_free(mdctx_md5) end
      if mdctx_sha256 ~= nil then libcrypto.EVP_MD_CTX_free(mdctx_sha256) end

   ::clean_up_short::
      if fh >= 0 then C.close(fh) end

   return fs_size,ret_2,ret_3

end

local function get_symlink(file)
   local line = ffi.new("char[?]",C.LINE_SIZE)
   if line == nil then return nil,"ffi.new failed " ..file end
   local size = C.readlink(file,line,C.LINE_SIZE-1)

   if size == -1 then return nil,C_error("readlink " .. file),ffi.errno() end

   return ffi.string(line,size)
end

local function check_1_mtree_file(pkg,fs_root,verbose,file)
   local line = ffi.new("char[?]",C.LINE_SIZE)
   if line == nil then return nil,"ffi.new failed " ..file end

   local gz_fh = libz.gzopen(file,"r")
   if gz_fh == nil then return nil,C_error("gzopen " .. file) end
   --libz.gzbuffer(gz_fh,65536)

   local file,root_file,size,md5,sha256
   local fs_size,symlink_linkto,link_fs
   local status,msg,aaa,bbb
   local l_line
   while(1) do
      if libz.gzgets(gz_fh,line,C.LINE_SIZE) == nil then break end
      
      l_line = ffi.string(line)
      file,size,md5,sha256 = string.match(l_line, "^(%S+) .* size=(%S+) md5digest=(%S+) sha256digest=(%S+)")
      if file and size and md5 and sha256 then
         if size then size=tonumber(size) end
      else
         file,symlink_linkto = string.match(l_line,"^(%S+) .* type=link link=(.*)%s$")
         if file and symlink_linkto then
            --printf_("symlink_name=%s symlink_linkto=%s",file,symlink_linkto)
            symlink_linkto = string.gsub(symlink_linkto,"\\%d%d%d",
               function(str) return string.char(tonumber(string.sub(str,2,-1),8)) end
            )
         else goto continue
         end
      end

      --print (file,size,md5,sha256)
      file = string.gsub(file,"^%./","",1)
      if file == '.PKGINFO' or file == '.INSTALL' or file == '.CHANGELOG' then
         goto continue
      end
      if file == '.BUILDINFO' then
         goto continue
      end

      file = string.gsub(file,"\\%d%d%d",
         function(str) return string.char(tonumber(string.sub(str,2,-1),8)) end
      )
      --print (file,size,md5,sha256)
      root_file = fs_root .. file

      if not size then
         --goto continue
         -- we are here so this is not a file, so maybe symlink or other type
         link_fs,aaa,bbb = get_symlink(root_file)
         -- status is nil so in aaa we have str_err and in bbb we have C.errno
         if link_fs == nil then
            if(bbb == C.ENOENT) then
            --if(string.match(msg,"No such file")) then
               if verbose > 0 then
                  printf_("EXTRA_MTREE pkg=%s symlink=%s link=%s",pkg,file,symlink_linkto)
               else
                  printf_("EXTRA_MTREE pkg=%s symlink=%s",pkg,file)
               end
            else
               printf_("FAIL check_symlink %s",aaa)
            end
         else
            if link_fs ~= symlink_linkto then
                  if verbose > 0 then
                     printf_("DIFF pkg=%s symlink=%s link_mtree=%s link_fs=%s",pkg,file,symlink_linkto,link_fs)
                  else
                     printf_("DIFF pkg=%s symlink=%s",pkg,file)
                  end
            end
         end

         goto continue
      end

      -- here we are in type=file case

      -- goto continue

      if verbose > 0 then fs_size,aaa,bbb = get_size_md5_sha256(root_file,-1)
      else fs_size,aaa,bbb = get_size_md5_sha256(root_file,size)
      end

      if not fs_size then
         -- fs_size is nil so in aaa we have str_err and in bbb we have C.errno
         if(bbb == C.ENOENT) then
         --if(string.match(msg,"No such file")) then
            if verbose > 0 then
               printf_("EXTRA_MTREE pkg=%s file=%s size_mtree=%s md5_mtree=%s sha256_mtree=%s",pkg,file,size,md5,sha256)
            else
               printf_("EXTRA_MTREE pkg=%s file=%s",pkg,file)
            end
         else
            printf_("FAIL get_size_md5_sha256 %s",aaa)
         end
         goto continue
      end

      --print(file,type(size),size,type(fs_size),fs_size,aaa,bbb)
      if fs_size ~= size or aaa ~= md5 or bbb ~= sha256 then
         if verbose > 0 then
            printf_("DIFF pkg=%s file=%s size_mtree=%d size_fs=%d md5_mtree=%s md5_fs=%s sha256_mtree=%s sha256_fs=%s",pkg,file,size,fs_size,md5,aaa,sha256,bbb)
         else 
            printf_("DIFF pkg=%s file=%s",pkg,file)
         end
         goto continue
      end

      
      --printf_("MATCH pkg=%s file=%s",pkg,file)
      
      --C.printf("LINE %s",line)

      ::continue::
   end
   
   libz.gzclose(ffi.gc(gz_fh,nil))

   return true
end


local function main(arg)
   local mtree_root = "/var/lib/pacman/local"
   local fs_root = "/"
   local pacman_list_pkg = "pacman -Q"
   local msg
   local check_all_pkgs = false
   local verbose = 0

   if not arg[1] then
      print("usage: " .. arg[0] .. " [ -a | pkg1 pkg2...] [-v | -vv] )")
      print("-a  = all pkgs")
      print("-v  = verbose(include size/md5/sha256)")
      print("-vv = even more verbose(show PKG= lines)")
      --printf_("%c%c%c@%04x.%c%c%c",99,103,109,255,110,101,116)
      
      return -1
   end

   for i,v in pairs(arg) do
      if i < 1 then goto continue end
      if v == '-v' then
         verbose = verbose + 1
         arg[i] = nil
      elseif v == '-vv' then
         verbose = verbose + 2
         arg[i] = nil
      elseif v == '-a' then
         check_all_pkgs = true
      elseif v == '-av' then
         check_all_pkgs = true
         verbose = verbose + 1
      elseif v == '-avv' then
         check_all_pkgs = true
         verbose = verbose + 2
      end
      ::continue::
   end
   
   --local file = io.stdin
   --if arg[1] and arg[1] ~= '-' then
   --   file,msg = io.open (arg[1])
   --   assert(file,msg)
   --end

   local fh_cmd,msg = io.popen(pacman_list_pkg,"r")
   if not fh_cmd then
      printf_("pacman cmd error %s",msg)
      return
   end

   for pkg in fh_cmd:lines() do
   --for pkg in io.stdin:lines() do
   --for pkg in lfs.dir(mtree_root) do
      if pkg == "." or pkg == ".." then goto continue end

      if not check_all_pkgs then
         local match = false
         for i,k in pairs(arg) do
            if i >= 1 and string.match(pkg,k) then
               match = true
               break
            end
         end
         if not match then goto continue end

      end
      pkg = string.gsub(pkg," ","-")
      --local pkg,ver = string.match(line, "^(%S+) (%S+)")
      if verbose > 1 then printf_("PKG=%s",pkg) end
      local ret,msg = check_1_mtree_file(pkg,fs_root,verbose,string.format("%s/%s/mtree",mtree_root,pkg))
      if not ret then printf_("FAIL mtree_file pkg=%s %s",pkg,msg) end
      
      ::continue::
   end

   fh_cmd:close()
   --if file ~= io.stdin then file:close() end
   
end
-----------------------------------------------------------
-----------------------------------------------------------
--print(check_symlink(arg[1],arg[2]))
--print(get_openssl_evp_md5_sha256_for_file(arg[1]))
return main(arg)
