~aleteoryx/muditaos

902a3a315eb8fd14bc8cadc827300f90f43a5152 — Mateusz Piesta 3 years ago 334b272
[MOS-806] Prepare scripts dependencies

* Added scripts dependencies
* Added script to generate scripts package
automatically
* Added integration with PureOS CMake
* [MOS-804] Created factory reset script
40 files changed, 2099 insertions(+), 0 deletions(-)

M CMakeLists.txt
A cmake/modules/AddScripts.cmake
M products/BellHybrid/CMakeLists.txt
M products/PurePhone/CMakeLists.txt
A scripts/lua/assets/BellHybrid/gui_image_factory_reset_failed.bin
A scripts/lua/assets/BellHybrid/gui_image_factory_reset_in_progress.bin
A scripts/lua/assets/BellHybrid/gui_image_factory_reset_success.bin
A scripts/lua/assets/BellHybrid/gui_image_update_failed.bin
A scripts/lua/assets/BellHybrid/gui_image_update_in_progress.bin
A scripts/lua/assets/BellHybrid/gui_image_update_success.bin
A scripts/lua/assets/PurePhone/gui_image_backup_failed.bin
A scripts/lua/assets/PurePhone/gui_image_backup_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_backup_success.bin
A scripts/lua/assets/PurePhone/gui_image_factory_reset_failed.bin
A scripts/lua/assets/PurePhone/gui_image_factory_reset_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_factory_reset_success.bin
A scripts/lua/assets/PurePhone/gui_image_keys_failed.bin
A scripts/lua/assets/PurePhone/gui_image_keys_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_keys_success.bin
A scripts/lua/assets/PurePhone/gui_image_recovery_failed.bin
A scripts/lua/assets/PurePhone/gui_image_recovery_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_recovery_success.bin
A scripts/lua/assets/PurePhone/gui_image_restore_failed.bin
A scripts/lua/assets/PurePhone/gui_image_restore_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_restore_success.bin
A scripts/lua/assets/PurePhone/gui_image_update_failed.bin
A scripts/lua/assets/PurePhone/gui_image_update_in_progress.bin
A scripts/lua/assets/PurePhone/gui_image_update_success.bin
A scripts/lua/backup.lua
A scripts/lua/entry.lua
A scripts/lua/factory.lua
A scripts/lua/install.sh
A scripts/lua/restore.lua
A scripts/lua/share/helpers.lua
A scripts/lua/share/ltar.lua
A scripts/lua/share/lunajson.lua
A scripts/lua/share/lunajson/decoder.lua
A scripts/lua/share/lunajson/encoder.lua
A scripts/lua/share/lunajson/sax.lua
A scripts/lua/update.lua
M CMakeLists.txt => CMakeLists.txt +7 -0
@@ 29,6 29,7 @@ include(AddPackage)
include(AutoModuleOption)
include(AddDirectories)
include(AddDatabases)
include(AddScripts)

message("Selected product: ${PRODUCT}")
message("Selected board:   ${BOARD}")


@@ 192,6 193,12 @@ add_databases_target(
        DEVEL ${WITH_DEVELOPMENT_FEATURES}
        DEPENDS create_databases_common
)
# Install scripts
add_scripts_target(
        TARGET install_scripts
        PRODUCT ${PRODUCT}
        DEST_DIR ${CMAKE_BINARY_DIR}/sysroot/system_a/scripts
)

add_library(version-header INTERFACE)
target_include_directories(version-header INTERFACE $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/source/include>)

A cmake/modules/AddScripts.cmake => cmake/modules/AddScripts.cmake +18 -0
@@ 0,0 1,18 @@
function(add_scripts_target)
    cmake_parse_arguments(
            _ARG
            ""
            "TARGET;DEST_DIR;PRODUCT"
            "DEPENDS"
            ${ARGN}
    )

    add_custom_target(
            ${_ARG_TARGET}
            DEPENDS ${_ARG_DEPENDS}

            COMMAND  ${PROJECT_SOURCE_DIR}/scripts/lua/install.sh ${_ARG_PRODUCT} ${_ARG_DEST_DIR}
            COMMENT
            "Installing scripts for ${_ARG_PRODUCT} to ${_ARG_DEST_DIR} directory"
    )
endfunction()
\ No newline at end of file

M products/BellHybrid/CMakeLists.txt => products/BellHybrid/CMakeLists.txt +2 -0
@@ 96,6 96,7 @@ if (${PROJECT_TARGET} STREQUAL "TARGET_RT1051")
        SYSROOT sysroot
        LUTS Luts.bin
        DEPENDS
            install_scripts
            create_product_databases
            create_user_directories
            assets


@@ 111,6 112,7 @@ else()
        SYSROOT sysroot
        LUTS ""
        DEPENDS
            install_scripts
            create_product_databases
            create_user_directories
            assets

M products/PurePhone/CMakeLists.txt => products/PurePhone/CMakeLists.txt +2 -0
@@ 121,6 121,7 @@ if (${PROJECT_TARGET} STREQUAL "TARGET_RT1051")
        SYSROOT sysroot
        LUTS Luts.bin
        DEPENDS
            install_scripts
            create_product_databases
            create_user_directories
            assets updater.bin-target


@@ 135,6 136,7 @@ else()
        SYSROOT sysroot
        LUTS ""
        DEPENDS
            install_scripts
            create_product_databases
            create_user_directories
            assets

A scripts/lua/assets/BellHybrid/gui_image_factory_reset_failed.bin => scripts/lua/assets/BellHybrid/gui_image_factory_reset_failed.bin +0 -0
A scripts/lua/assets/BellHybrid/gui_image_factory_reset_in_progress.bin => scripts/lua/assets/BellHybrid/gui_image_factory_reset_in_progress.bin +0 -0
A scripts/lua/assets/BellHybrid/gui_image_factory_reset_success.bin => scripts/lua/assets/BellHybrid/gui_image_factory_reset_success.bin +0 -0
A scripts/lua/assets/BellHybrid/gui_image_update_failed.bin => scripts/lua/assets/BellHybrid/gui_image_update_failed.bin +0 -0
A scripts/lua/assets/BellHybrid/gui_image_update_in_progress.bin => scripts/lua/assets/BellHybrid/gui_image_update_in_progress.bin +0 -0
A scripts/lua/assets/BellHybrid/gui_image_update_success.bin => scripts/lua/assets/BellHybrid/gui_image_update_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_backup_failed.bin => scripts/lua/assets/PurePhone/gui_image_backup_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_backup_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_backup_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_backup_success.bin => scripts/lua/assets/PurePhone/gui_image_backup_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_factory_reset_failed.bin => scripts/lua/assets/PurePhone/gui_image_factory_reset_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_factory_reset_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_factory_reset_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_factory_reset_success.bin => scripts/lua/assets/PurePhone/gui_image_factory_reset_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_keys_failed.bin => scripts/lua/assets/PurePhone/gui_image_keys_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_keys_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_keys_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_keys_success.bin => scripts/lua/assets/PurePhone/gui_image_keys_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_recovery_failed.bin => scripts/lua/assets/PurePhone/gui_image_recovery_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_recovery_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_recovery_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_recovery_success.bin => scripts/lua/assets/PurePhone/gui_image_recovery_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_restore_failed.bin => scripts/lua/assets/PurePhone/gui_image_restore_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_restore_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_restore_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_restore_success.bin => scripts/lua/assets/PurePhone/gui_image_restore_success.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_update_failed.bin => scripts/lua/assets/PurePhone/gui_image_update_failed.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_update_in_progress.bin => scripts/lua/assets/PurePhone/gui_image_update_in_progress.bin +0 -0
A scripts/lua/assets/PurePhone/gui_image_update_success.bin => scripts/lua/assets/PurePhone/gui_image_update_success.bin +0 -0
A scripts/lua/backup.lua => scripts/lua/backup.lua +16 -0
@@ 0,0 1,16 @@
local helpers = require('helpers')
local recovery = require('recovery')
local tar = require('ltar')
local lfs = require('lfs')
local backup = {}

backup.script_name = "backup.lua"
backup.img_in_progress = "assets/gui_image_backup_in_progress.bin"
backup.img_success = "assets/gui_image_backup_success.bin"
backup.img_failure = "assets/gui_image_backup_failed.bin"

function backup.execute()
end

return backup


A scripts/lua/entry.lua => scripts/lua/entry.lua +86 -0
@@ 0,0 1,86 @@
package.path = package.path .. ";share/?.lua"

local helpers = require('helpers')

local rec = require('recovery')
local backup = require('backup')
local restore = require('restore')
local update = require('update')
local factory = require('factory')

local scripts = {}
scripts[rec.sys.boot_reason_codes.backup] = backup
scripts[rec.sys.boot_reason_codes.restore] = restore
scripts[rec.sys.boot_reason_codes.update] = update
scripts[rec.sys.boot_reason_codes.factory] = factory

--- Display image
-- @function display_image
-- @param path path to raw image data
local function display_image(path)
    local raw_data = helpers.read_whole_file(path)
    rec.gui.clear()
    rec.gui.display_raw_img(600, 480, raw_data)
end

local function print_recovery_info()
    print(string.format("PureRecovery version: %s, branch: %s, revision: %s", rec.version(), rec.branch(),
        rec.revision()))
end

local function print_boot_reason()
    print(string.format("Boot reason: %s", rec.sys.boot_reason_str()))
end

local function generate_report_file(boot_reason, success, message)
    local file_path = rec.sys.source_slot() .. "/log/recovery_status.json"
    local body = string.format(
        "{\"version\": \"%s\",\"branch\": \"%s\",\"revision\": \"%s\",\"boot_reason\": %d,\"result\": %s,\"message\": \"%s\"}",
        rec.version(), rec.branch(), rec.revision(), boot_reason, tostring(success), message)
    local fd = io.open(file_path, 'w')
    fd:write(body)
    fd:close()
end

local function handle_boot_reason()
    local boot_reason = rec.sys.boot_reason()
    rec.sys.set_boot_reason(rec.sys.boot_reason_codes.os)
    return boot_reason
end

local function handle_script_success(boot_reason)
    display_image(scripts[boot_reason].img_success)
    print("Finished successfully")
end

local function handle_script_failure(boot_reason, message)
    print(message)
    display_image(scripts[boot_reason].img_failure)
end

local function handle_exit(boot_reason, status, message)
    generate_report_file(boot_reason, status, message)
    rec.sys.sleep(1)
    rec.gui.clear()
end

local function invoke_script(boot_reason)
    print(string.format("Executing '%s' script...", scripts[boot_reason].script_name))
    display_image(scripts[boot_reason].img_in_progress)

    local status, message = pcall(scripts[boot_reason].execute)
    if status then
        handle_script_success(boot_reason)
        return true, "ok"
    else
        handle_script_failure(boot_reason, message)
        return false, message
    end
end

print_recovery_info()
print_boot_reason()
local boot_reason = handle_boot_reason()
local status, message = invoke_script(boot_reason)
handle_exit(boot_reason, status, message)


A scripts/lua/factory.lua => scripts/lua/factory.lua +39 -0
@@ 0,0 1,39 @@
local helpers = require('helpers')
local recovery = require('recovery')

local factory = {}
local user_dir = recovery.sys.user()
local system_dir = recovery.sys.source_slot()
local db_dir = system_dir .. "/db"
local db_factory_dir = system_dir .. "/db/factory"
local file_indexer_cache = system_dir .. "/data/.directory_is_indexed"

factory.script_name = "factory.lua"
factory.img_in_progress = "assets/gui_image_factory_reset_in_progress.bin"
factory.img_success = "assets/gui_image_factory_reset_success.bin"
factory.img_failure = "assets/gui_image_factory_reset_failed.bin"

local function remove_old_databases()
    print(string.format("Removing old databases from '%s'", db_dir))
    helpers.rm_files_from_dir(db_dir)
end

local function copy_clean_databases()
    print(string.format("Copying clean databases from '%s' to '%s'", db_factory_dir, db_dir))
    helpers.copy_dir(db_factory_dir, db_dir)
end

local function remove_cache()
    if helpers.exists(file_indexer_cache) then
        print(string.format("Removing cache file '%s'", file_indexer_cache))
        assert(os.remove(file_indexer_cache))
    end
end

function factory.execute()
    remove_old_databases()
    copy_clean_databases()
    remove_cache()
end

return factory

A scripts/lua/install.sh => scripts/lua/install.sh +36 -0
@@ 0,0 1,36 @@
#!/usr/bin/env bash
# Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

function validate_product_selection() {
    case ${PRODUCT,,} in
        pure | purephone)
            PRODUCT_SHORTNAME="pure"
            PRODUCT="PurePhone"
            return 0 ;;
        bell | bellhybrid)
            PRODUCT_SHORTNAME="bell"
            PRODUCT="BellHybrid"
            return 0 ;;
        *)
            echo "Wrong product: \"${PRODUCT}\""
    esac
}

PRODUCT=$1
DESTINATION=$2

if validate_product_selection; then
    if [ -d ${DESTINATION} ]; then
        rm -Rf ${DESTINATION}
    fi
    parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
    cd "$parent_path"

    mkdir -p ${DESTINATION}
    cp -r assets/${PRODUCT} ${DESTINATION}/assets/
    cp -r share ${DESTINATION}/share
    cp -r migration/migration.lua ${DESTINATION}/share
    cp *.lua ${DESTINATION}
    cd -
fi
\ No newline at end of file

A scripts/lua/restore.lua => scripts/lua/restore.lua +17 -0
@@ 0,0 1,17 @@
local helpers = require('helpers')
local recovery = require('recovery')
local ltar = require('ltar')
local lfs = require('lfs')
local json = require('lunajson')
local migration = require('migration')
local restore = {}

restore.script_name = "restore.lua"
restore.img_in_progress = "assets/gui_image_restore_in_progress.bin"
restore.img_success = "assets/gui_image_restore_success.bin"
restore.img_failure = "assets/gui_image_restore_failed.bin"

function restore.execute()
end

return restore

A scripts/lua/share/helpers.lua => scripts/lua/share/helpers.lua +202 -0
@@ 0,0 1,202 @@
-- Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
--- helper scripts
-- @module helpers
local lfs = require('lfs')

local helpers = {}

local function dirtree(dir)
    assert(dir and dir ~= "", "directory parameter is missing or empty")
    if string.sub(dir, -1) == "/" then
        dir = string.sub(dir, 1, -2)
    end

    local function yieldtree(dir)
        for entry in lfs.dir(dir) do
            if entry ~= "." and entry ~= ".." then
                entry = dir .. "/" .. entry
                local attr = lfs.attributes(entry)
                coroutine.yield(entry, attr)
                if attr.mode == "directory" then
                    yieldtree(entry)
                end
            end
        end
    end

    return coroutine.wrap(function()
        yieldtree(dir)
    end)
end

local function ends_with(str, ending)
    return ending == "" or str:sub(-#ending) == ending
end

local function starts_with(str, start)
    return str:sub(1, #start) == start
end

local function build_path(prefix, name)
    if not ends_with(prefix, '/') then
        prefix = prefix .. '/'
    end
    if starts_with(name, '/') then
        name = name:sub(2, -1)
    end
    return prefix .. name
end

local function strip_from_prefix(prefix,path)
    local name = path:gsub(prefix, "")
    name = name:sub(2, -1)
    return name
end

local function basedir(p)
    return p:gsub('[^\\/]+[\\/]?$', '')
end

--- Copy file
-- @function copy_file
-- @param filename_in input file
-- @param filename_out output file
function helpers.copy_file(filename_in, filename_out)
    local size = 1024 * 512
    local size_in = lfs.attributes(filename_in, "size")
    local fd_in = assert(io.open(filename_in, "r"))
    local fd_out = assert(io.open(filename_out, "w"))
    while true do
        local block = fd_in:read(size)
        if not block then
            break
        end
        assert(fd_out:write(block))
    end
    fd_in:close()
    fd_out:close()
end

--- Remove directory and its contents
-- @function rmdir
-- @param dir directory to remove
function helpers.rmdir(dir)
    for file in lfs.dir(dir) do
        local file_path = dir .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                assert(os.remove(file_path))
            elseif lfs.attributes(file_path, "mode") == "directory" then
                helpers.rmdir(file_path)
            end
        end
    end
    lfs.rmdir(dir)
end

--- Remove all files from directory without touching internal directories
-- @function rm_files_from_dir
-- @param dir directory to remove files from
function helpers.rm_files_from_dir(dir)
    for file in lfs.dir(dir) do
        local file_path = dir .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                assert(os.remove(file_path))
            end
        end
    end
end

--- Copy directory recursively
-- @function copy_dir
-- @param from source directory
-- @param where target directory
function helpers.copy_dir(from, where)
    for filename, attr in dirtree(from) do
        local name = filename:gsub(from, "")
        if attr.mode == "directory" then
            assert(lfs.mkdir(build_path(where, name)))
        else
            helpers.copy_file(filename, build_path(where, name))
        end
    end
end

--- Get the size of specified directory using regex filter
-- @function dir_size_filtered
-- @param path directory path
-- @param matcher regex expression
-- @return total size of directory in bytes
function helpers.dir_size_filtered(path, filter)
    local total_size = 0
    for filename, attr in dirtree(path) do
        local name = strip_from_prefix(path,filename)
        if name:match(filter) then
            total_size = total_size + attr.size
        end
    end
    return total_size
end

--- Get the size of specified directory
-- @function dir_size
-- @param path directory path
-- @return total size of directory in bytes
function helpers.dir_size(path)
    return helpers.dir_size_regex('.*')
end

--- Check if specified dir/file exists
-- @function exists
-- @param path directory or file path
-- @return true or false
function helpers.exists(path)
    local ret = lfs.attributes(path)
    return ret ~= nil
end

--- Read the whole file at once 
-- @function read_whole_file
-- @param file file path
-- @return file contents
function helpers.read_whole_file(file)
    local f = assert(io.open(file, "rb"))
    local content = assert(f:read("*a"))
    f:close()
    return content
end

--- Extract filename from filename path
-- @function get_filename
-- @param file file path
-- @return filename
function helpers.get_filename(file)
    return file:match("^.+/(.+)$")
end

--- Extract file extension from filename
-- @function get_file_extension
-- @param file file path
-- @return file extension
function helpers.get_file_extension(file)
    return file:match("^.+(%..+)$")
end

function helpers.mkdirp(p)
    if lfs.attributes(p, 'mode') == 'directory' then
        return nil, 'already exists'
    end

    local b = basedir(p)
    if #b > 0 and lfs.attributes(b, 'mode') ~= 'directory' then
        local r, m = helpers.mkdirp(b)
        if not r then
            return r, m
        end
    end
    return lfs.mkdir(p)
end

return helpers

A scripts/lua/share/ltar.lua => scripts/lua/share/ltar.lua +233 -0
@@ 0,0 1,233 @@
--- LUA support for TAR packages
-- @module ltar
local microtar = require("lmicrotar")
local lfs = require("lfs")

local tar = {}

local function strip_from_prefix(prefix, filename)
    local name = filename:gsub(prefix, "")
    name = name:sub(2, -1)
    return name
end

local function get_filename(file)
    return file:match("^.+/(.+)$")
end

local function basedir(p)
    return p:gsub('[^\\/]+[\\/]?$', '')
end

local function mkdirp(p)
    if lfs.attributes(p, 'mode') == 'directory' then
        return nil, 'already exists'
    end

    local b = basedir(p)
    if #b > 0 and lfs.attributes(b, 'mode') ~= 'directory' then
        local r, m = mkdirp(b)
        if not r then
            return r, m
        end
    end
    return lfs.mkdir(p)
end

local function dirtree(dir)
    assert(dir and dir ~= "", "directory parameter is missing or empty")
    if string.sub(dir, -1) == "/" then
        dir = string.sub(dir, 1, -2)
    end

    local function yieldtree(dir)
        for entry in lfs.dir(dir) do
            if entry ~= "." and entry ~= ".." then
                entry = dir .. "/" .. entry
                local attr = lfs.attributes(entry)
                coroutine.yield(entry, attr)
                if attr.mode == "directory" then
                    yieldtree(entry)
                end
            end
        end
    end

    return coroutine.wrap(function()
        yieldtree(dir)
    end)
end

local function write_tarfile_chunks(handle, fd)
    local size = 1024 * 512
    while true do
        local block = fd:read(size)
        if not block then
            break
        end
        handle:write_data(block, block:len())
    end
end

local function read_tarfile_chunks(handle, fd, total_size)
    local block_size = 1024 * 512
    local to_read = {};

    while total_size > 0 do
        if total_size > block_size then
            to_read = block_size
        else
            to_read = total_size
        end
        fd:write(handle:read_data(to_read))
        total_size = total_size - to_read
    end
end

--- Create empty tar file
-- @function create
-- @param path where to put newly created tar file
-- @return tar handle or nil
function tar.create(path)
    Handle = {
        handle = microtar.open(path, "w"),
        add_file = function(self, filename)
            local size = lfs.attributes(filename, "size")
            local fd = io.open(filename, "r")
            self.handle:write_file_header(filename, size)
            write_tarfile_chunks(self.handle, fd)
            fd:close()
        end,
        add_directory = function(self, path)
            self.handle:write_dir_header(path)
        end,
        close = function(self)
            self.handle:close()
        end
    }
    return Handle
end

--- Iterate over contents of tar file
-- @function iter_by_handle
-- @param handle tar handle create by @{create}
function tar.iter_by_handle(handle)
    return function()
        local f = handle:read_header()
        if f then
            handle:next()
            return f
        end
    end
end

--- Iterate over contents of tar file
-- @function iter_by_path
-- @param path path to tar file
function tar.iter_by_path(path)
    local handle = microtar.open(path)
    return function()
        local f = handle:read_header()
        if f then
            handle:next()
            return f
        end
    end
end

--- Pack contents of specified dir to tar file
-- @function create_from_path
-- @param path directory 
-- @param where where to save tar file 
function tar.create_from_path(path, where)
    tar.create_from_path_match(path, where, ".*")
end

--- Pack contents of specified dir to tar file using regex
-- @function create_from_path_regex
-- @param path directory 
-- @param where where to save tar file 
-- @param matcher regex expression
function tar.create_from_path_regex(path, where, matcher)
    local handle = microtar.open(where, "w")
    for filename, attr in dirtree(path) do
        local name = strip_from_prefix(path, filename)
        if name:match(matcher) then
            if attr.mode == "directory" then
                handle:write_dir_header(name)
            else
                handle:write_file_header(name, attr.size)
                local fd = io.open(filename, "r")
                write_tarfile_chunks(handle, fd)
                fd:close()
            end
        end
    end
    handle:close()
end

--- Unpack tar file to specified directory
-- @function unpack
-- @param path directory 
-- @param where where to store unpacked files 
function tar.unpack(path, where)
    local handle = microtar.open(path)
    local header = handle:read_header()
    while header do
        if header.type == microtar.TDIR then
            mkdirp(where .. "/" .. header.name)
        elseif header.type == microtar.TREG then
            local fd = io.open(where .. "/" .. header.name, "w")
            read_tarfile_chunks(handle, fd, header.size)
            fd:close()
        end
        handle:next()
        header = handle:read_header()
    end
    handle:close()
end

--- Append directory to already existing tar file
-- @function append
-- @param path directory 
-- @param where location of already existing tar file 
function tar.append(path, where)
    local handle = microtar.open(where, "a")
    for filename, attr in dirtree(path) do
        if attr.mode == "directory" then
            handle:write_dir_header(filename)
        else
            handle:write_file_header(filename, attr.size)
            local fd = io.open(filename, "r")
            write_tarfile_chunks(handle, fd)
            fd:close()
        end
    end
    handle:close()
end

--- Append file to already existing tar file
-- @function append_file
-- @param path file path 
-- @param where location of already existing tar file 
function tar.append_file(path, where)
    local filename = get_filename(path)
    local size = lfs.attributes(path, 'size')
    local handle = microtar.open(where, "a")
    handle:write_file_header(filename, size)
    local fd = io.open(path, "r")
    write_tarfile_chunks(handle, fd)
    fd:close()
    handle:close()
end

function tar.find(tar_path, what)
    local handle = microtar.open(tar_path)
    local stats = handle:find(what)
    if stats ~= nil then
        return handle:read_data()
    end
    return nil
end

return tar

A scripts/lua/share/lunajson.lua => scripts/lua/share/lunajson.lua +11 -0
@@ 0,0 1,11 @@
local newdecoder = require 'lunajson.decoder'
local newencoder = require 'lunajson.encoder'
local sax = require 'lunajson.sax'
-- If you need multiple contexts of decoder and/or encoder,
-- you can require lunajson.decoder and/or lunajson.encoder directly.
return {
	decode = newdecoder(),
	encode = newencoder(),
	newparser = sax.newparser,
	newfileparser = sax.newfileparser,
}

A scripts/lua/share/lunajson/decoder.lua => scripts/lua/share/lunajson/decoder.lua +515 -0
@@ 0,0 1,515 @@
local setmetatable, tonumber, tostring =
      setmetatable, tonumber, tostring
local floor, inf =
      math.floor, math.huge
local mininteger, tointeger =
      math.mininteger or nil, math.tointeger or nil
local byte, char, find, gsub, match, sub =
      string.byte, string.char, string.find, string.gsub, string.match, string.sub

local function _decode_error(pos, errmsg)
	error("parse error at " .. pos .. ": " .. errmsg, 2)
end

local f_str_ctrl_pat
if _VERSION == "Lua 5.1" then
	-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
	f_str_ctrl_pat = '[^\32-\255]'
else
	f_str_ctrl_pat = '[\0-\31]'
end

local _ENV = nil


local function newdecoder()
	local json, pos, nullv, arraylen, rec_depth

	-- `f` is the temporary for dispatcher[c] and
	-- the dummy for the first return value of `find`
	local dispatcher, f

	--[[
		Helper
	--]]
	local function decode_error(errmsg)
		return _decode_error(pos, errmsg)
	end

	--[[
		Invalid
	--]]
	local function f_err()
		decode_error('invalid value')
	end

	--[[
		Constants
	--]]
	-- null
	local function f_nul()
		if sub(json, pos, pos+2) == 'ull' then
			pos = pos+3
			return nullv
		end
		decode_error('invalid value')
	end

	-- false
	local function f_fls()
		if sub(json, pos, pos+3) == 'alse' then
			pos = pos+4
			return false
		end
		decode_error('invalid value')
	end

	-- true
	local function f_tru()
		if sub(json, pos, pos+2) == 'rue' then
			pos = pos+3
			return true
		end
		decode_error('invalid value')
	end

	--[[
		Numbers
		Conceptually, the longest prefix that matches to `[-+.0-9A-Za-z]+` (in regexp)
		is captured as a number and its conformance to the JSON spec is checked.
	--]]
	-- deal with non-standard locales
	local radixmark = match(tostring(0.5), '[^0-9]')
	local fixedtonumber = tonumber
	if radixmark ~= '.' then
		if find(radixmark, '%W') then
			radixmark = '%' .. radixmark
		end
		fixedtonumber = function(s)
			return tonumber(gsub(s, '.', radixmark))
		end
	end

	local function number_error()
		return decode_error('invalid number')
	end

	-- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?`
	local function f_zro(mns)
		local num, c = match(json, '^(%.?[0-9]*)([-+.A-Za-z]?)', pos)  -- skipping 0

		if num == '' then
			if c == '' then
				if mns then
					return -0.0
				end
				return 0
			end

			if c == 'e' or c == 'E' then
				num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
				if c == '' then
					pos = pos + #num
					if mns then
						return -0.0
					end
					return 0.0
				end
			end
			number_error()
		end

		if byte(num) ~= 0x2E or byte(num, -1) == 0x2E then
			number_error()
		end

		if c ~= '' then
			if c == 'e' or c == 'E' then
				num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
			end
			if c ~= '' then
				number_error()
			end
		end

		pos = pos + #num
		c = fixedtonumber(num)

		if mns then
			c = -c
		end
		return c
	end

	-- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?`
	local function f_num(mns)
		pos = pos-1
		local num, c = match(json, '^([0-9]+%.?[0-9]*)([-+.A-Za-z]?)', pos)
		if byte(num, -1) == 0x2E then  -- error if ended with period
			number_error()
		end

		if c ~= '' then
			if c ~= 'e' and c ~= 'E' then
				number_error()
			end
			num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
			if not num or c ~= '' then
				number_error()
			end
		end

		pos = pos + #num
		c = fixedtonumber(num)

		if mns then
			c = -c
			if c == mininteger and not find(num, '[^0-9]') then
				c = mininteger
			end
		end
		return c
	end

	-- skip minus sign
	local function f_mns()
		local c = byte(json, pos)
		if c then
			pos = pos+1
			if c > 0x30 then
				if c < 0x3A then
					return f_num(true)
				end
			else
				if c > 0x2F then
					return f_zro(true)
				end
			end
		end
		decode_error('invalid number')
	end

	--[[
		Strings
	--]]
	local f_str_hextbl = {
		0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
		0x8, 0x9, inf, inf, inf, inf, inf, inf,
		inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF,
		__index = function()
			return inf
		end
	}
	setmetatable(f_str_hextbl, f_str_hextbl)

	local f_str_escapetbl = {
		['"']  = '"',
		['\\'] = '\\',
		['/']  = '/',
		['b']  = '\b',
		['f']  = '\f',
		['n']  = '\n',
		['r']  = '\r',
		['t']  = '\t',
		__index = function()
			decode_error("invalid escape sequence")
		end
	}
	setmetatable(f_str_escapetbl, f_str_escapetbl)

	local function surrogate_first_error()
		return decode_error("1st surrogate pair byte not continued by 2nd")
	end

	local f_str_surrogate_prev = 0
	local function f_str_subst(ch, ucode)
		if ch == 'u' then
			local c1, c2, c3, c4, rest = byte(ucode, 1, 5)
			ucode = f_str_hextbl[c1-47] * 0x1000 +
			        f_str_hextbl[c2-47] * 0x100 +
			        f_str_hextbl[c3-47] * 0x10 +
			        f_str_hextbl[c4-47]
			if ucode ~= inf then
				if ucode < 0x80 then  -- 1byte
					if rest then
						return char(ucode, rest)
					end
					return char(ucode)
				elseif ucode < 0x800 then  -- 2bytes
					c1 = floor(ucode / 0x40)
					c2 = ucode - c1 * 0x40
					c1 = c1 + 0xC0
					c2 = c2 + 0x80
					if rest then
						return char(c1, c2, rest)
					end
					return char(c1, c2)
				elseif ucode < 0xD800 or 0xE000 <= ucode then  -- 3bytes
					c1 = floor(ucode / 0x1000)
					ucode = ucode - c1 * 0x1000
					c2 = floor(ucode / 0x40)
					c3 = ucode - c2 * 0x40
					c1 = c1 + 0xE0
					c2 = c2 + 0x80
					c3 = c3 + 0x80
					if rest then
						return char(c1, c2, c3, rest)
					end
					return char(c1, c2, c3)
				elseif 0xD800 <= ucode and ucode < 0xDC00 then  -- surrogate pair 1st
					if f_str_surrogate_prev == 0 then
						f_str_surrogate_prev = ucode
						if not rest then
							return ''
						end
						surrogate_first_error()
					end
					f_str_surrogate_prev = 0
					surrogate_first_error()
				else  -- surrogate pair 2nd
					if f_str_surrogate_prev ~= 0 then
						ucode = 0x10000 +
						        (f_str_surrogate_prev - 0xD800) * 0x400 +
						        (ucode - 0xDC00)
						f_str_surrogate_prev = 0
						c1 = floor(ucode / 0x40000)
						ucode = ucode - c1 * 0x40000
						c2 = floor(ucode / 0x1000)
						ucode = ucode - c2 * 0x1000
						c3 = floor(ucode / 0x40)
						c4 = ucode - c3 * 0x40
						c1 = c1 + 0xF0
						c2 = c2 + 0x80
						c3 = c3 + 0x80
						c4 = c4 + 0x80
						if rest then
							return char(c1, c2, c3, c4, rest)
						end
						return char(c1, c2, c3, c4)
					end
					decode_error("2nd surrogate pair byte appeared without 1st")
				end
			end
			decode_error("invalid unicode codepoint literal")
		end
		if f_str_surrogate_prev ~= 0 then
			f_str_surrogate_prev = 0
			surrogate_first_error()
		end
		return f_str_escapetbl[ch] .. ucode
	end

	-- caching interpreted keys for speed
	local f_str_keycache = setmetatable({}, {__mode="v"})

	local function f_str(iskey)
		local newpos = pos
		local tmppos, c1, c2
		repeat
			newpos = find(json, '"', newpos, true)  -- search '"'
			if not newpos then
				decode_error("unterminated string")
			end
			tmppos = newpos-1
			newpos = newpos+1
			c1, c2 = byte(json, tmppos-1, tmppos)
			if c2 == 0x5C and c1 == 0x5C then  -- skip preceding '\\'s
				repeat
					tmppos = tmppos-2
					c1, c2 = byte(json, tmppos-1, tmppos)
				until c2 ~= 0x5C or c1 ~= 0x5C
				tmppos = newpos-2
			end
		until c2 ~= 0x5C  -- leave if '"' is not preceded by '\'

		local str = sub(json, pos, tmppos)
		pos = newpos

		if iskey then  -- check key cache
			tmppos = f_str_keycache[str]  -- reuse tmppos for cache key/val
			if tmppos then
				return tmppos
			end
			tmppos = str
		end

		if find(str, f_str_ctrl_pat) then
			decode_error("unescaped control string")
		end
		if find(str, '\\', 1, true) then  -- check whether a backslash exists
			-- We need to grab 4 characters after the escape char,
			-- for encoding unicode codepoint to UTF-8.
			-- As we need to ensure that every first surrogate pair byte is
			-- immediately followed by second one, we grab upto 5 characters and
			-- check the last for this purpose.
			str = gsub(str, '\\(.)([^\\]?[^\\]?[^\\]?[^\\]?[^\\]?)', f_str_subst)
			if f_str_surrogate_prev ~= 0 then
				f_str_surrogate_prev = 0
				decode_error("1st surrogate pair byte not continued by 2nd")
			end
		end
		if iskey then  -- commit key cache
			f_str_keycache[tmppos] = str
		end
		return str
	end

	--[[
		Arrays, Objects
	--]]
	-- array
	local function f_ary()
		rec_depth = rec_depth + 1
		if rec_depth > 1000 then
			decode_error('too deeply nested json (> 1000)')
		end
		local ary = {}

		pos = match(json, '^[ \n\r\t]*()', pos)

		local i = 0
		if byte(json, pos) == 0x5D then  -- check closing bracket ']' which means the array empty
			pos = pos+1
		else
			local newpos = pos
			repeat
				i = i+1
				f = dispatcher[byte(json,newpos)]  -- parse value
				pos = newpos+1
				ary[i] = f()
				newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos)  -- check comma
			until not newpos

			newpos = match(json, '^[ \n\r\t]*%]()', pos)  -- check closing bracket
			if not newpos then
				decode_error("no closing bracket of an array")
			end
			pos = newpos
		end

		if arraylen then -- commit the length of the array if `arraylen` is set
			ary[0] = i
		end
		rec_depth = rec_depth - 1
		return ary
	end

	-- objects
	local function f_obj()
		rec_depth = rec_depth + 1
		if rec_depth > 1000 then
			decode_error('too deeply nested json (> 1000)')
		end
		local obj = {}

		pos = match(json, '^[ \n\r\t]*()', pos)
		if byte(json, pos) == 0x7D then  -- check closing bracket '}' which means the object empty
			pos = pos+1
		else
			local newpos = pos

			repeat
				if byte(json, newpos) ~= 0x22 then  -- check '"'
					decode_error("not key")
				end
				pos = newpos+1
				local key = f_str(true)  -- parse key

				-- optimized for compact json
				-- c1, c2 == ':', <the first char of the value> or
				-- c1, c2, c3 == ':', ' ', <the first char of the value>
				f = f_err
				local c1, c2, c3 = byte(json, pos, pos+3)
				if c1 == 0x3A then
					if c2 ~= 0x20 then
						f = dispatcher[c2]
						newpos = pos+2
					else
						f = dispatcher[c3]
						newpos = pos+3
					end
				end
				if f == f_err then  -- read a colon and arbitrary number of spaces
					newpos = match(json, '^[ \n\r\t]*:[ \n\r\t]*()', pos)
					if not newpos then
						decode_error("no colon after a key")
					end
					f = dispatcher[byte(json, newpos)]
					newpos = newpos+1
				end
				pos = newpos
				obj[key] = f()  -- parse value
				newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos)
			until not newpos

			newpos = match(json, '^[ \n\r\t]*}()', pos)
			if not newpos then
				decode_error("no closing bracket of an object")
			end
			pos = newpos
		end

		rec_depth = rec_depth - 1
		return obj
	end

	--[[
		The jump table to dispatch a parser for a value,
		indexed by the code of the value's first char.
		Nil key means the end of json.
	--]]
	dispatcher = { [0] =
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err,
		f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num,
		f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err,
		f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err,
		f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err,
		__index = function()
			decode_error("unexpected termination")
		end
	}
	setmetatable(dispatcher, dispatcher)

	--[[
		run decoder
	--]]
	local function decode(json_, pos_, nullv_, arraylen_)
		json, pos, nullv, arraylen = json_, pos_, nullv_, arraylen_
		rec_depth = 0

		pos = match(json, '^[ \n\r\t]*()', pos)

		f = dispatcher[byte(json, pos)]
		pos = pos+1
		local v = f()

		if pos_ then
			return v, pos
		else
			f, pos = find(json, '^[ \n\r\t]*', pos)
			if pos ~= #json then
				decode_error('json ended')
			end
			return v
		end
	end

	return decode
end

return newdecoder

A scripts/lua/share/lunajson/encoder.lua => scripts/lua/share/lunajson/encoder.lua +185 -0
@@ 0,0 1,185 @@
local error = error
local byte, find, format, gsub, match = string.byte, string.find, string.format,  string.gsub, string.match
local concat = table.concat
local tostring = tostring
local pairs, type = pairs, type
local setmetatable = setmetatable
local huge, tiny = 1/0, -1/0

local f_string_esc_pat
if _VERSION == "Lua 5.1" then
	-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
	f_string_esc_pat = '[^ -!#-[%]^-\255]'
else
	f_string_esc_pat = '[\0-\31"\\]'
end

local _ENV = nil


local function newencoder()
	local v, nullv
	local i, builder, visited

	local function f_tostring(v)
		builder[i] = tostring(v)
		i = i+1
	end

	local radixmark = match(tostring(0.5), '[^0-9]')
	local delimmark = match(tostring(12345.12345), '[^0-9' .. radixmark .. ']')
	if radixmark == '.' then
		radixmark = nil
	end

	local radixordelim
	if radixmark or delimmark then
		radixordelim = true
		if radixmark and find(radixmark, '%W') then
			radixmark = '%' .. radixmark
		end
		if delimmark and find(delimmark, '%W') then
			delimmark = '%' .. delimmark
		end
	end

	local f_number = function(n)
		if tiny < n and n < huge then
			local s = format("%.17g", n)
			if radixordelim then
				if delimmark then
					s = gsub(s, delimmark, '')
				end
				if radixmark then
					s = gsub(s, radixmark, '.')
				end
			end
			builder[i] = s
			i = i+1
			return
		end
		error('invalid number')
	end

	local doencode

	local f_string_subst = {
		['"'] = '\\"',
		['\\'] = '\\\\',
		['\b'] = '\\b',
		['\f'] = '\\f',
		['\n'] = '\\n',
		['\r'] = '\\r',
		['\t'] = '\\t',
		__index = function(_, c)
			return format('\\u00%02X', byte(c))
		end
	}
	setmetatable(f_string_subst, f_string_subst)

	local function f_string(s)
		builder[i] = '"'
		if find(s, f_string_esc_pat) then
			s = gsub(s, f_string_esc_pat, f_string_subst)
		end
		builder[i+1] = s
		builder[i+2] = '"'
		i = i+3
	end

	local function f_table(o)
		if visited[o] then
			error("loop detected")
		end
		visited[o] = true

		local tmp = o[0]
		if type(tmp) == 'number' then -- arraylen available
			builder[i] = '['
			i = i+1
			for j = 1, tmp do
				doencode(o[j])
				builder[i] = ','
				i = i+1
			end
			if tmp > 0 then
				i = i-1
			end
			builder[i] = ']'

		else
			tmp = o[1]
			if tmp ~= nil then -- detected as array
				builder[i] = '['
				i = i+1
				local j = 2
				repeat
					doencode(tmp)
					tmp = o[j]
					if tmp == nil then
						break
					end
					j = j+1
					builder[i] = ','
					i = i+1
				until false
				builder[i] = ']'

			else -- detected as object
				builder[i] = '{'
				i = i+1
				local tmp = i
				for k, v in pairs(o) do
					if type(k) ~= 'string' then
						error("non-string key")
					end
					f_string(k)
					builder[i] = ':'
					i = i+1
					doencode(v)
					builder[i] = ','
					i = i+1
				end
				if i > tmp then
					i = i-1
				end
				builder[i] = '}'
			end
		end

		i = i+1
		visited[o] = nil
	end

	local dispatcher = {
		boolean = f_tostring,
		number = f_number,
		string = f_string,
		table = f_table,
		__index = function()
			error("invalid type value")
		end
	}
	setmetatable(dispatcher, dispatcher)

	function doencode(v)
		if v == nullv then
			builder[i] = 'null'
			i = i+1
			return
		end
		return dispatcher[type(v)](v)
	end

	local function encode(v_, nullv_)
		v, nullv = v_, nullv_
		i, builder, visited = 1, {}, {}

		doencode(v)
		return concat(builder)
	end

	return encode
end

return newencoder

A scripts/lua/share/lunajson/sax.lua => scripts/lua/share/lunajson/sax.lua +719 -0
@@ 0,0 1,719 @@
local setmetatable, tonumber, tostring =
      setmetatable, tonumber, tostring
local floor, inf =
      math.floor, math.huge
local mininteger, tointeger =
      math.mininteger or nil, math.tointeger or nil
local byte, char, find, gsub, match, sub =
      string.byte, string.char, string.find, string.gsub, string.match, string.sub

local function _parse_error(pos, errmsg)
	error("parse error at " .. pos .. ": " .. errmsg, 2)
end

local f_str_ctrl_pat
if _VERSION == "Lua 5.1" then
	-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
	f_str_ctrl_pat = '[^\32-\255]'
else
	f_str_ctrl_pat = '[\0-\31]'
end

local type, unpack = type, table.unpack or unpack
local open = io.open

local _ENV = nil


local function nop() end

local function newparser(src, saxtbl)
	local json, jsonnxt, rec_depth
	local jsonlen, pos, acc = 0, 1, 0

	-- `f` is the temporary for dispatcher[c] and
	-- the dummy for the first return value of `find`
	local dispatcher, f

	-- initialize
	if type(src) == 'string' then
		json = src
		jsonlen = #json
		jsonnxt = function()
			json = ''
			jsonlen = 0
			jsonnxt = nop
		end
	else
		jsonnxt = function()
			acc = acc + jsonlen
			pos = 1
			repeat
				json = src()
				if not json then
					json = ''
					jsonlen = 0
					jsonnxt = nop
					return
				end
				jsonlen = #json
			until jsonlen > 0
		end
		jsonnxt()
	end

	local sax_startobject = saxtbl.startobject or nop
	local sax_key = saxtbl.key or nop
	local sax_endobject = saxtbl.endobject or nop
	local sax_startarray = saxtbl.startarray or nop
	local sax_endarray = saxtbl.endarray or nop
	local sax_string = saxtbl.string or nop
	local sax_number = saxtbl.number or nop
	local sax_boolean = saxtbl.boolean or nop
	local sax_null = saxtbl.null or nop

	--[[
		Helper
	--]]
	local function tryc()
		local c = byte(json, pos)
		if not c then
			jsonnxt()
			c = byte(json, pos)
		end
		return c
	end

	local function parse_error(errmsg)
		return _parse_error(acc + pos, errmsg)
	end

	local function tellc()
		return tryc() or parse_error("unexpected termination")
	end

	local function spaces()  -- skip spaces and prepare the next char
		while true do
			pos = match(json, '^[ \n\r\t]*()', pos)
			if pos <= jsonlen then
				return
			end
			if jsonlen == 0 then
				parse_error("unexpected termination")
			end
			jsonnxt()
		end
	end

	--[[
		Invalid
	--]]
	local function f_err()
		parse_error('invalid value')
	end

	--[[
		Constants
	--]]
	-- fallback slow constants parser
	local function generic_constant(target, targetlen, ret, sax_f)
		for i = 1, targetlen do
			local c = tellc()
			if byte(target, i) ~= c then
				parse_error("invalid char")
			end
			pos = pos+1
		end
		return sax_f(ret)
	end

	-- null
	local function f_nul()
		if sub(json, pos, pos+2) == 'ull' then
			pos = pos+3
			return sax_null(nil)
		end
		return generic_constant('ull', 3, nil, sax_null)
	end

	-- false
	local function f_fls()
		if sub(json, pos, pos+3) == 'alse' then
			pos = pos+4
			return sax_boolean(false)
		end
		return generic_constant('alse', 4, false, sax_boolean)
	end

	-- true
	local function f_tru()
		if sub(json, pos, pos+2) == 'rue' then
			pos = pos+3
			return sax_boolean(true)
		end
		return generic_constant('rue', 3, true, sax_boolean)
	end

	--[[
		Numbers
		Conceptually, the longest prefix that matches to `[-+.0-9A-Za-z]+` (in regexp)
		is captured as a number and its conformance to the JSON spec is checked.
	--]]
	-- deal with non-standard locales
	local radixmark = match(tostring(0.5), '[^0-9]')
	local fixedtonumber = tonumber
	if radixmark ~= '.' then
		if find(radixmark, '%W') then
			radixmark = '%' .. radixmark
		end
		fixedtonumber = function(s)
			return tonumber(gsub(s, '.', radixmark))
		end
	end

	local function number_error()
		return parse_error('invalid number')
	end

	-- fallback slow parser
	local function generic_number(mns)
		local buf = {}
		local i = 1
		local is_int = true

		local c = byte(json, pos)
		pos = pos+1

		local function nxt()
			buf[i] = c
			i = i+1
			c = tryc()
			pos = pos+1
		end

		if c == 0x30 then
			nxt()
			if c and 0x30 <= c and c < 0x3A then
				number_error()
			end
		else
			repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
		end
		if c == 0x2E then
			is_int = false
			nxt()
			if not (c and 0x30 <= c and c < 0x3A) then
				number_error()
			end
			repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
		end
		if c == 0x45 or c == 0x65 then
			is_int = false
			nxt()
			if c == 0x2B or c == 0x2D then
				nxt()
			end
			if not (c and 0x30 <= c and c < 0x3A) then
				number_error()
			end
			repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
		end
		if c and (0x41 <= c and c <= 0x5B or
		          0x61 <= c and c <= 0x7B or
		          c == 0x2B or c == 0x2D or c == 0x2E) then
			number_error()
		end
		pos = pos-1

		local num = char(unpack(buf))
		num = fixedtonumber(num)
		if mns then
			num = -num
			if num == mininteger and is_int then
				num = mininteger
			end
		end
		return sax_number(num)
	end

	-- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?`
	local function f_zro(mns)
		local num, c = match(json, '^(%.?[0-9]*)([-+.A-Za-z]?)', pos)  -- skipping 0

		if num == '' then
			if pos > jsonlen then
				pos = pos - 1
				return generic_number(mns)
			end
			if c == '' then
				if mns then
					return sax_number(-0.0)
				end
				return sax_number(0)
			end

			if c == 'e' or c == 'E' then
				num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
				if c == '' then
					pos = pos + #num
					if pos > jsonlen then
						pos = pos - #num - 1
						return generic_number(mns)
					end
					if mns then
						return sax_number(-0.0)
					end
					return sax_number(0.0)
				end
			end
			pos = pos-1
			return generic_number(mns)
		end

		if byte(num) ~= 0x2E or byte(num, -1) == 0x2E then
			pos = pos-1
			return generic_number(mns)
		end

		if c ~= '' then
			if c == 'e' or c == 'E' then
				num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
			end
			if c ~= '' then
				pos = pos-1
				return generic_number(mns)
			end
		end

		pos = pos + #num
		if pos > jsonlen then
			pos = pos - #num - 1
			return generic_number(mns)
		end
		c = fixedtonumber(num)

		if mns then
			c = -c
		end
		return sax_number(c)
	end

	-- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?`
	local function f_num(mns)
		pos = pos-1
		local num, c = match(json, '^([0-9]+%.?[0-9]*)([-+.A-Za-z]?)', pos)
		if byte(num, -1) == 0x2E then  -- error if ended with period
			return generic_number(mns)
		end

		if c ~= '' then
			if c ~= 'e' and c ~= 'E' then
				return generic_number(mns)
			end
			num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
			if not num or c ~= '' then
				return generic_number(mns)
			end
		end

		pos = pos + #num
		if pos > jsonlen then
			pos = pos - #num
			return generic_number(mns)
		end
		c = fixedtonumber(num)

		if mns then
			c = -c
			if c == mininteger and not find(num, '[^0-9]') then
				c = mininteger
			end
		end
		return sax_number(c)
	end

	-- skip minus sign
	local function f_mns()
		local c = byte(json, pos) or tellc()
		if c then
			pos = pos+1
			if c > 0x30 then
				if c < 0x3A then
					return f_num(true)
				end
			else
				if c > 0x2F then
					return f_zro(true)
				end
			end
		end
		parse_error("invalid number")
	end

	--[[
		Strings
	--]]
	local f_str_hextbl = {
		0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
		0x8, 0x9, inf, inf, inf, inf, inf, inf,
		inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, inf, inf, inf, inf, inf, inf, inf,
		inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF,
		__index = function()
			return inf
		end
	}
	setmetatable(f_str_hextbl, f_str_hextbl)

	local f_str_escapetbl = {
		['"']  = '"',
		['\\'] = '\\',
		['/']  = '/',
		['b']  = '\b',
		['f']  = '\f',
		['n']  = '\n',
		['r']  = '\r',
		['t']  = '\t',
		__index = function()
			parse_error("invalid escape sequence")
		end
	}
	setmetatable(f_str_escapetbl, f_str_escapetbl)

	local function surrogate_first_error()
		return parse_error("1st surrogate pair byte not continued by 2nd")
	end

	local f_str_surrogate_prev = 0
	local function f_str_subst(ch, ucode)
		if ch == 'u' then
			local c1, c2, c3, c4, rest = byte(ucode, 1, 5)
			ucode = f_str_hextbl[c1-47] * 0x1000 +
			        f_str_hextbl[c2-47] * 0x100 +
			        f_str_hextbl[c3-47] * 0x10 +
			        f_str_hextbl[c4-47]
			if ucode ~= inf then
				if ucode < 0x80 then  -- 1byte
					if rest then
						return char(ucode, rest)
					end
					return char(ucode)
				elseif ucode < 0x800 then  -- 2bytes
					c1 = floor(ucode / 0x40)
					c2 = ucode - c1 * 0x40
					c1 = c1 + 0xC0
					c2 = c2 + 0x80
					if rest then
						return char(c1, c2, rest)
					end
					return char(c1, c2)
				elseif ucode < 0xD800 or 0xE000 <= ucode then  -- 3bytes
					c1 = floor(ucode / 0x1000)
					ucode = ucode - c1 * 0x1000
					c2 = floor(ucode / 0x40)
					c3 = ucode - c2 * 0x40
					c1 = c1 + 0xE0
					c2 = c2 + 0x80
					c3 = c3 + 0x80
					if rest then
						return char(c1, c2, c3, rest)
					end
					return char(c1, c2, c3)
				elseif 0xD800 <= ucode and ucode < 0xDC00 then  -- surrogate pair 1st
					if f_str_surrogate_prev == 0 then
						f_str_surrogate_prev = ucode
						if not rest then
							return ''
						end
						surrogate_first_error()
					end
					f_str_surrogate_prev = 0
					surrogate_first_error()
				else  -- surrogate pair 2nd
					if f_str_surrogate_prev ~= 0 then
						ucode = 0x10000 +
						        (f_str_surrogate_prev - 0xD800) * 0x400 +
						        (ucode - 0xDC00)
						f_str_surrogate_prev = 0
						c1 = floor(ucode / 0x40000)
						ucode = ucode - c1 * 0x40000
						c2 = floor(ucode / 0x1000)
						ucode = ucode - c2 * 0x1000
						c3 = floor(ucode / 0x40)
						c4 = ucode - c3 * 0x40
						c1 = c1 + 0xF0
						c2 = c2 + 0x80
						c3 = c3 + 0x80
						c4 = c4 + 0x80
						if rest then
							return char(c1, c2, c3, c4, rest)
						end
						return char(c1, c2, c3, c4)
					end
					parse_error("2nd surrogate pair byte appeared without 1st")
				end
			end
			parse_error("invalid unicode codepoint literal")
		end
		if f_str_surrogate_prev ~= 0 then
			f_str_surrogate_prev = 0
			surrogate_first_error()
		end
		return f_str_escapetbl[ch] .. ucode
	end

	local function f_str(iskey)
		local pos2 = pos
		local newpos
		local str = ''
		local bs
		while true do
			while true do  -- search '\' or '"'
				newpos = find(json, '[\\"]', pos2)
				if newpos then
					break
				end
				str = str .. sub(json, pos, jsonlen)
				if pos2 == jsonlen+2 then
					pos2 = 2
				else
					pos2 = 1
				end
				jsonnxt()
				if jsonlen == 0 then
					parse_error("unterminated string")
				end
			end
			if byte(json, newpos) == 0x22 then  -- break if '"'
				break
			end
			pos2 = newpos+2  -- skip '\<char>'
			bs = true  -- mark the existence of a backslash
		end
		str = str .. sub(json, pos, newpos-1)
		pos = newpos+1

		if find(str, f_str_ctrl_pat) then
			parse_error("unescaped control string")
		end
		if bs then  -- a backslash exists
			-- We need to grab 4 characters after the escape char,
			-- for encoding unicode codepoint to UTF-8.
			-- As we need to ensure that every first surrogate pair byte is
			-- immediately followed by second one, we grab upto 5 characters and
			-- check the last for this purpose.
			str = gsub(str, '\\(.)([^\\]?[^\\]?[^\\]?[^\\]?[^\\]?)', f_str_subst)
			if f_str_surrogate_prev ~= 0 then
				f_str_surrogate_prev = 0
				parse_error("1st surrogate pair byte not continued by 2nd")
			end
		end

		if iskey then
			return sax_key(str)
		end
		return sax_string(str)
	end

	--[[
		Arrays, Objects
	--]]
	-- arrays
	local function f_ary()
		rec_depth = rec_depth + 1
		if rec_depth > 1000 then
			parse_error('too deeply nested json (> 1000)')
		end
		sax_startarray()

		spaces()
		if byte(json, pos) == 0x5D then  -- check closing bracket ']' which means the array empty
			pos = pos+1
		else
			local newpos
			while true do
				f = dispatcher[byte(json, pos)]  -- parse value
				pos = pos+1
				f()
				newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos)  -- check comma
				if newpos then
					pos = newpos
				else
					newpos = match(json, '^[ \n\r\t]*%]()', pos)  -- check closing bracket
					if newpos then
						pos = newpos
						break
					end
					spaces()  -- since the current chunk can be ended, skip spaces toward following chunks
					local c = byte(json, pos)
					pos = pos+1
					if c == 0x2C then  -- check comma again
						spaces()
					elseif c == 0x5D then  -- check closing bracket again
						break
					else
						parse_error("no closing bracket of an array")
					end
				end
				if pos > jsonlen then
					spaces()
				end
			end
		end

		rec_depth = rec_depth - 1
		return sax_endarray()
	end

	-- objects
	local function f_obj()
		rec_depth = rec_depth + 1
		if rec_depth > 1000 then
			parse_error('too deeply nested json (> 1000)')
		end
		sax_startobject()

		spaces()
		if byte(json, pos) == 0x7D then  -- check closing bracket '}' which means the object empty
			pos = pos+1
		else
			local newpos
			while true do
				if byte(json, pos) ~= 0x22 then
					parse_error("not key")
				end
				pos = pos+1
				f_str(true)  -- parse key
				newpos = match(json, '^[ \n\r\t]*:[ \n\r\t]*()', pos)  -- check colon
				if newpos then
					pos = newpos
				else
					spaces()  -- read spaces through chunks
					if byte(json, pos) ~= 0x3A then  -- check colon again
						parse_error("no colon after a key")
					end
					pos = pos+1
					spaces()
				end
				if pos > jsonlen then
					spaces()
				end
				f = dispatcher[byte(json, pos)]
				pos = pos+1
				f()  -- parse value
				newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos)  -- check comma
				if newpos then
					pos = newpos
				else
					newpos = match(json, '^[ \n\r\t]*}()', pos)  -- check closing bracket
					if newpos then
						pos = newpos
						break
					end
					spaces()  -- read spaces through chunks
					local c = byte(json, pos)
					pos = pos+1
					if c == 0x2C then  -- check comma again
						spaces()
					elseif c == 0x7D then  -- check closing bracket again
						break
					else
						parse_error("no closing bracket of an object")
					end
				end
				if pos > jsonlen then
					spaces()
				end
			end
		end

		rec_depth = rec_depth - 1
		return sax_endobject()
	end

	--[[
		The jump table to dispatch a parser for a value,
		indexed by the code of the value's first char.
		Key should be non-nil.
	--]]
	dispatcher = { [0] =
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err,
		f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num,
		f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err,
		f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err,
		f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err,
		f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err,
	}

	--[[
		public funcitons
	--]]
	local function run()
		rec_depth = 0
		spaces()
		f = dispatcher[byte(json, pos)]
		pos = pos+1
		f()
	end

	local function read(n)
		if n < 0 then
			error("the argument must be non-negative")
		end
		local pos2 = (pos-1) + n
		local str = sub(json, pos, pos2)
		while pos2 > jsonlen and jsonlen ~= 0 do
			jsonnxt()
			pos2 = pos2 - (jsonlen - (pos-1))
			str = str .. sub(json, pos, pos2)
		end
		if jsonlen ~= 0 then
			pos = pos2+1
		end
		return str
	end

	local function tellpos()
		return acc + pos
	end

	return {
		run = run,
		tryc = tryc,
		read = read,
		tellpos = tellpos,
	}
end

local function newfileparser(fn, saxtbl)
	local fp = open(fn)
	local function gen()
		local s
		if fp then
			s = fp:read(8192)
			if not s then
				fp:close()
				fp = nil
			end
		end
		return s
	end
	return newparser(gen, saxtbl)
end

return {
	newparser = newparser,
	newfileparser = newfileparser
}

A scripts/lua/update.lua => scripts/lua/update.lua +11 -0
@@ 0,0 1,11 @@
local update = {}

update.script_name = "update.lua"
update.img_in_progress = "assets/gui_image_update_in_progress.bin"
update.img_success = "assets/gui_image_update_success.bin"
update.img_failure = "assets/gui_image_update_failed.bin"

function update.execute()
end

return update
\ No newline at end of file