--[[---------------
Noki v0.4
-------------------
Noki is a toolkit to convert Nokia PC Suite backuped SMS to a
unicode .txt file, which is more accessible than the original
.nfb or .nfc.

It works well for Nokia PC Suite 6.5.12 and my mobile phone is 
Nokia 7360. There might be some compatibility problem if you
use earlier version of the PC Suite.

How to use:
 noki.save_sms('nokia.nfb', 'sms.txt')

Under the MIT license.

Noki is a part of LuaBit(http://luaforge.net/projects/bit/).

copyright(c) 2006~2007 hanzhao (abrash_han@hotmail.com)
--]]---------------

require 'hex'
require 'bit'

do
-- globals
local RETURN = '\13\0\10\0'
local SMS_FILE = '\\MPAPI\\MESSAGES'
local SMS_INBOX = 'PIT_MESSAGE_INBOX'
local SMS_OUTBOX = 'PIT_MESSAGE_OUTBOX'
local SMS_ARCHIVEBOX = 'PIT_MESSAGE_ARCHIVE'

-- output decorator
local SMS_INBOX_DEC = '[INBOX] '
local SMS_OUTBOX_DEC = '[OUTBOX] '
local SMS_ARCHIVE_DEC = '[ARCHIVE] '

-- box type
local BoxType = {
 NON = 0,
 IN = 1,
 OUT = 2,
 ARCHIVE = 3,
}

-- feed each char with an extra \0
local function asci_to_uni(asci)
 --print("-------")
 local uni = ""
 for i = 1, string.len(asci) do
  local str = string.format('%c\0', string.byte(asci, i))
  --print(string.len(str))
  uni = uni .. str

 end
 return uni
end

local function asci_padding(asci, pad)
 local uni = ""
 for i = 1, string.len(asci) do
  local str = string.format('%c', string.byte(asci, i))
  --print(string.len(str))
  uni = uni .. str .. pad

 end
 return uni
end

-- shrink the \0 in uni code string
local function uni_to_asci(uni)
 local asci = ''
 --print('uni len ' .. string.len(uni))
 for i = 1, string.len(uni), 2 do
  asci = asci .. string.sub(uni, i, i)
 end
 return asci
end

local function reader(str)
 local index = 1
 return function (n)
         --print('reader>>> idx ' .. index .. " n " .. n)
         local sub = string.sub(str, index, index + n - 1)
         --[[print(hex.to_hex(string.byte(sub, 1)))
         print(hex.to_hex(string.byte(sub, 2)))
         print(hex.to_hex(string.byte(sub, 3)))
         print(hex.to_hex(string.byte(sub, 4)))
         --]]
         index = index + n
         return sub
        end
end

local function read_number(read, n)
 local str = read(n)
 local rslt = 0
 for i = 1, n do
  local v = string.byte(str, i)
  rslt = bit.bor(rslt, bit.blshift(v, (i-1)*8))
 end
 return rslt
end

local function read_int(read)
 return read_number(read, 4)
end


local function read_short(read)
 return read_number(read, 2)
end

local function read_nfb_string(read)
 local len = read_int(read)
 local unistr = read(len*2)
 return unistr
end

local function read_nfb_header(read)
 local nfb_header = {
  ver = read_int(read),
  firmware = read_nfb_string(read),
  phone = read_nfb_string(read),
  entries = read_int(read),
 }
 return nfb_header
end

local function read_nfb_file(read)
 local nfbf = {}
 nfbf.path = read_nfb_string(read)

 nfbf.nbytes = read_int(read)

 nfbf.bytes = read(nfbf.nbytes)
 local stamp = read_int(read)

 return nfbf
end

local function read_nfb_dir(read)
 local nfbd = {
  path = read_nfb_string(read)
 }
 return nfbd
end

local function save_entry(fp, tbl)
 for k, v in pairs(tbl) do
  fp:write(v)
  fp:write(RETURN)
 end
end

-- save sms entries
local function save_sms(fp, ctnt)
 -- print("save sms ----")
 local in_box = asci_padding(SMS_INBOX, "%z")
 local out_box = asci_padding(SMS_OUTBOX, "%z")
 local archive_box = asci_padding(SMS_ARCHIVEBOX, "%z")
 local line_s = asci_padding("1020", "%z")
 local head = asci_padding("1033", "%z")
 local tail = asci_padding("1040", "%z")
 local service_center_tail = asci_padding("1080", "%z")
 local phone_nb = "%+%z%d%z%d%z[%d%z]+" -- default is type 145 with '+'
 local phone_nb_129 = string.rep("%d%z", 11) -- phone number type 129 without '+'
 local time = "[%d%z]+%-%z%d%z%d%z%-%z%d%z%d%zT%z%d%z%d%z:%z%d%z%d%z"

 local pattern = "([^\10]+)\13%z\10%z"
 local line_end = "\13%z\10%z"
 local lineb, linee = string.find(ctnt, line_end)
 local start = 1
 local line_number = 1
 while(lineb and linee) do
  local line = string.sub(ctnt, start, lineb - 1)
  --line = string.sub(ctnt, gb, ge)
  local type = BoxType.NON
  --print('capture ' .. string.len(line))
  --print(uni_to_asci(box))
  if(string.find(line, in_box)) then
   fp:write(asci_to_uni(SMS_INBOX_DEC))
   type = BoxType.IN
  elseif(string.find(line, out_box)) then
   fp:write(asci_to_uni(SMS_OUTBOX_DEC))
   type = BoxType.OUT
  elseif(string.find(line, archive_box)) then
   fp:write(asci_to_uni(SMS_ARCHIVE_DEC))
   type = BoxType.ARCHIVE
  else
   --print(uni_to_asci(line))
   io.close(fp)
   --error('unknown sms type')

   return
  end

   hb, he = string.find(line, head)
   tb, te = string.find(line, tail)

   local first_number = ""
   -- service center address
   sb, se = string.find(line, phone_nb, tb)
   --print("" .. sb .. ", " .. se)
   if(sb and se) then
    --print(uni_to_asci(string.sub(line, sb, se)))
    -- keep the find number, if the second find for sender address fails
    -- then this number is the sender address
    first_number = string.sub(line, sb, se)
   else
    sb, se = string.find(line, phone_nb_129, tb)
    if(not (sb and se)) then
	  --io.close(fp)
	  --error("error service center address")
	  --return
	  first_number = "empty number"
	 -- nokia's pc suite may leave the serivce center address empty
    end
   end
   
   -- sender address
   se_old = se
   sb, se = string.find(line, phone_nb, se)
   --print("" .. sb .. ", " .. se)
   local sender_address = ""
   if(sb and se) then
    --print(uni_to_asci(string.sub(line, sb, se)))
    sender_address = string.sub(line, sb, se)
   else
    sb, se = string.find(line, phone_nb_129, se_old)
    if(not (sb and se)) then
	 --[[
     print(line_number)
	 io.close(fp)
	 error("error sender address")
	 --]]
	 sender_address = first_number
    end
   end
   -- write sender
   fp:write(sender_address)
   fp:write(" \0")

   -- date time
   -- out box have no date time slot
   if(type ~= BoxType.OUT and first_number ~= "empty number") then
    tmb, tme = string.find(line, time, se)
    --print('' .. tmb .. ", " .. tme)
    if(tmb and tme) then
     --print(uni_to_asci(string.sub(line, tmb+1, tme)))
	else
	 io.close(fp)
	 error("error reading date time")
	 return
    end
    fp:write(string.sub(line, tmb+1, tme))
   end
   fp:write(RETURN)

   fp:write(string.sub(line, he+3, tb-3))
   
   fp:write(RETURN)
   fp:write(RETURN)
  --end
  start = linee + 1
  lineb, linee = string.find(ctnt, line_end, linee)
  line_number = line_number + 1
 end
end

-- save sms from a .nfc or .nfb file to a unicode .txt file
local function save_nfx_to(from, too)
	local fp = io.open(too, 'wb')
	if(not fp) then
	 error("error opening file " .. too .. " to write")
	 return
	end
	v = string.format('%c%c', 255, 254)
	-- unicode .txt 'FF FE'
	fp:write(v)

	-- read the .nfc file
	local nokia = io.open(from, 'rb')
	if(not nokia) then
	 error("error open file " .. from .. " to read")
	end
	local ctnt = nokia:read("*a")
	io.close(nokia)

	local read = reader(ctnt)

	local header = read_nfb_header(read)
	--print(header.ver)
	--print(header.entries)

	for i=1, header.entries do
	 --print(i)
	 local type = read_int(read)
	 if(type == 1) then
	  -- file entry
	  --print('file')
	  local fe = read_nfb_file(read)
	  --save_entry(fp, fe)
	  if(uni_to_asci(fe.path) == SMS_FILE) then
	   local smsctnt = fe.bytes
	   --print('sms len ' .. fe.nbytes)
	   save_sms(fp, smsctnt)
	   return
	  end

	 elseif(type == 2) then
	  -- dir entry
	  --print('dir')
	  local fd = read_nfb_dir(read)
	  --save_entry(fp, fd)
	 else
	  io.close(fp)
	  error('unknown entry type : ' .. hex.to_hex(type))
	 end
	end
	io.close(fp)
end

-- noki interface --
noki = {
 save_sms = save_nfx_to
}

end -- end block

-- sample
-- noki.save_sms('nokia2.nfb', 'sms2.txt')