跳转到内容

模組:沙盒/a2569875/PythonScript/Librarys/Codecs

维基百科,自由的百科全书
-- Module:PythonScript/Librarys/Codecs
local Codecs = {}
local PyClass = require("Module:沙盒/a2569875/PythonScript/Class")
local Runtime = require("Module:沙盒/a2569875/PythonScript/Runtime")

local function normalize_encoding(enc)
    enc = tostring(enc or "utf-8"):lower():gsub("_", "-"):gsub(" ", "-")
    if enc == "utf8" or enc == "u8" or enc == "utf" or enc == "utf-8-sig" then return "utf-8" end
    if enc == "ascii" or enc == "us-ascii" then return "ascii" end
    if enc == "latin1" or enc == "latin-1" or enc == "iso-8859-1" then return "latin-1" end
    return enc
end

local error_handlers = {}

function Codecs.register_error(name, handler)
    if type(name) ~= "string" then Runtime.raise("TypeError", "register_error() argument 1 must be str") end
    error_handlers[name] = handler
end

function Codecs.lookup_error(name)
    local h = error_handlers[name]
    if not h then Runtime.raise("LookupError", "unknown error handler name '" .. tostring(name) .. "'") end
    return h
end

-- ==========================================
-- 1. 內建錯誤處理器 (回傳: 替換字串, 1-based 下一個 Lua 索引)
-- ==========================================
error_handlers["strict"] = function(exc)
    Runtime.raise(exc)
end

error_handlers["ignore"] = function(exc)
    return "", exc.__dict._lua_end
end

error_handlers["replace"] = function(exc)
    if exc.__class.__name == "UnicodeDecodeError" then
        return "\239\191\189", exc.__dict._lua_end -- U+FFFD () 的標準 UTF-8 bytes
    else
        return "?", exc.__dict._lua_end
    end
end

error_handlers["backslashreplace"] = function(exc)
    local out = {}
    local data = exc.__dict._raw_data
    if exc.__class.__name == "UnicodeDecodeError" then
        for i = exc.__dict._lua_start, exc.__dict._lua_end - 1 do
            table.insert(out, string.format("\\x%02x", string.byte(data, i)))
        end
    else
        local cp = mw.ustring.codepoint(data, exc.__dict._lua_start)
        if cp <= 0xFF then table.insert(out, string.format("\\x%02x", cp))
        elseif cp <= 0xFFFF then table.insert(out, string.format("\\u%04x", cp))
        else table.insert(out, string.format("\\U%08x", cp)) end
    end
    return table.concat(out), exc.__dict._lua_end
end

error_handlers["xmlcharrefreplace"] = function(exc)
    if exc.__class.__name == "UnicodeDecodeError" then
        Runtime.raise("TypeError", "don't know how to handle UnicodeDecodeError in error callback")
    end
    local cp = mw.ustring.codepoint(exc.__dict._raw_data, exc.__dict._lua_start)
    return string.format("&#%d;", cp), exc.__dict._lua_end
end

error_handlers["surrogateescape"] = function(exc)
    local data = exc.__dict._raw_data
    if exc.__class.__name == "UnicodeDecodeError" then
        local out = {}
        for i = exc.__dict._lua_start, exc.__dict._lua_end - 1 do
            local b = string.byte(data, i)
            local cp = 0xDC00 + b
            out[#out + 1] = string.char(0xED, 0xB0 + math.floor((cp - 0xD800) / 64), 0x80 + (cp % 64))
        end
        return table.concat(out), exc.__dict._lua_end
    else
        local cp = mw.ustring.codepoint(data, exc.__dict._lua_start)
        if cp >= 0xDC80 and cp <= 0xDCFF then
            return string.char(cp - 0xDC00), exc.__dict._lua_end
        end
        Runtime.raise(exc)
    end
end

-- ==========================================
-- 2. 例外建構與自定義處理器路由
-- ==========================================
-- 注意:start_idx 與 end_idx 傳入時為 Lua 1-based,對外屬性 (start/end/start_idx/end_idx) 統一轉為 Python 0-based!
local function build_decode_exc(encoding, raw_data, start_idx, end_idx, reason)
    local exc = PyClass.make_instance(Runtime.builtins.UnicodeDecodeError)
    local py_bytes = { __type = "bytes", _data = raw_data }
    local s0, e0 = start_idx - 1, end_idx - 1
    exc.__dict.encoding = encoding
    exc.__dict.object = py_bytes
    exc.__dict.start = s0
    exc.__dict["end"] = e0
    exc.__dict.start_idx = s0
    exc.__dict.end_idx = e0
    exc.__dict._raw_data = raw_data
    exc.__dict._lua_start = start_idx
    exc.__dict._lua_end = end_idx
    exc.__dict.reason = reason
    local safe_enc = encoding:sub(1, 400)
    exc.__dict.msg = string.format("'%s' codec can't decode byte 0x%02x in position %d: %s", safe_enc, string.byte(raw_data, start_idx), s0, reason)
    exc.__dict.args = { __type = "tuple", elements = { encoding, py_bytes, s0, e0, reason } }
    return exc
end

local function build_encode_exc(encoding, raw_str, start_idx, end_idx, reason)
    local exc = PyClass.make_instance(Runtime.builtins.UnicodeEncodeError)
    local s0, e0 = start_idx - 1, end_idx - 1
    exc.__dict.encoding = encoding
    exc.__dict.object = raw_str
    exc.__dict.start = s0
    exc.__dict["end"] = e0
    exc.__dict.start_idx = s0
    exc.__dict.end_idx = e0
    exc.__dict._raw_data = raw_str
    exc.__dict._lua_start = start_idx
    exc.__dict._lua_end = end_idx
    exc.__dict.reason = reason
    local cp = mw.ustring.codepoint(raw_str, start_idx)
    local cp_hex = string.format(cp <= 0xFFFF and "\\u%04x" or "\\U%08x", cp)
    local safe_enc = encoding:sub(1, 400)
    exc.__dict.msg = string.format("'%s' codec can't encode character '%s' in position %d: %s", safe_enc, cp_hex, s0, reason)
    exc.__dict.args = { __type = "tuple", elements = { encoding, raw_str, s0, e0, reason } }
    return exc
end

local function invoke_error_handler(errors, exc)
    local h = error_handlers[errors]
    if not h then
        Runtime.raise("LookupError", "unknown error handler name '" .. tostring(errors) .. "'")
    end
    if type(h) == "function" then
        return h(exc)
    else
        local Evaluator = require("Module:沙盒/a2569875/PythonScript/Evaluator")
        local ret = Evaluator.invoke(h, { exc }, {})
        if type(ret) ~= "table" or ret.__type ~= "tuple" or #ret.elements ~= 2 then
            Runtime.raise("TypeError", "encoding/decoding error handler must return (str/bytes, int) tuple")
        end
        local repl = ret.elements[1]
        if type(repl) == "table" and repl.__type == "bytes" then repl = repl._data end
        local new_pos_0 = tonumber(ret.elements[2]) or exc.__dict.end_idx
        return tostring(repl), new_pos_0 + 1
    end
end

-- ==========================================
-- 3. UTF-8 / ASCII / Latin-1 狀態機
-- ==========================================
local function decode_utf8(data, errors, strip_bom)
    local i, len = 1, #data
    if strip_bom and len >= 3 and data:sub(1, 3) == "\239\187\191" then
        i = 4
    end
    local out = {}
    while i <= len do
        local b1 = string.byte(data, i)
        if b1 <= 0x7F then
            out[#out + 1] = string.char(b1); i = i + 1
        elseif b1 >= 0xC2 and b1 <= 0xDF and i + 1 <= len then
            local b2 = string.byte(data, i + 1)
            if b2 >= 0x80 and b2 <= 0xBF then
                out[#out + 1] = string.char(b1, b2); i = i + 2
            else
                local exc = build_decode_exc("utf-8", data, i, i + 1, "invalid continuation byte")
                local repl, next_i = invoke_error_handler(errors, exc)
                out[#out + 1] = repl; i = next_i
            end
        elseif b1 >= 0xE0 and b1 <= 0xEF and i + 2 <= len then
            local b2, b3 = string.byte(data, i + 1), string.byte(data, i + 2)
            local valid = false
            if b1 == 0xE0 and b2 >= 0xA0 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF then valid = true
            elseif b1 >= 0xE1 and b1 <= 0xEC and b2 >= 0x80 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF then valid = true
            elseif b1 == 0xED and b2 >= 0x80 and b2 <= 0x9F and b3 >= 0x80 and b3 <= 0xBF then valid = true
            elseif b1 >= 0xEE and b1 <= 0xEF and b2 >= 0x80 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF then valid = true end
            if valid then
                out[#out + 1] = string.char(b1, b2, b3); i = i + 3
            else
                local err_len = ((b1 == 0xE0 and b2 >= 0xA0 and b2 <= 0xBF) or (b1 >= 0xE1 and b1 <= 0xEC and b2 >= 0x80 and b2 <= 0xBF) or (b1 == 0xED and b2 >= 0x80 and b2 <= 0x9F) or (b1 >= 0xEE and b1 <= 0xEF and b2 >= 0x80 and b2 <= 0xBF)) and 2 or 1
                local exc = build_decode_exc("utf-8", data, i, i + err_len, "invalid continuation byte")
                local repl, next_i = invoke_error_handler(errors, exc)
                out[#out + 1] = repl; i = next_i
            end
        elseif b1 >= 0xF0 and b1 <= 0xF4 and i + 3 <= len then
            local b2, b3, b4 = string.byte(data, i + 1), string.byte(data, i + 2), string.byte(data, i + 3)
            local valid = false
            if b1 == 0xF0 and b2 >= 0x90 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF and b4 >= 0x80 and b4 <= 0xBF then valid = true
            elseif b1 >= 0xF1 and b1 <= 0xF3 and b2 >= 0x80 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF and b4 >= 0x80 and b4 <= 0xBF then valid = true
            elseif b1 == 0xF4 and b2 >= 0x80 and b2 <= 0x8F and b3 >= 0x80 and b3 <= 0xBF and b4 >= 0x80 and b4 <= 0xBF then valid = true end
            if valid then
                out[#out + 1] = string.char(b1, b2, b3, b4); i = i + 4
            else
                local err_len = 1
                if (b1 == 0xF0 and b2 >= 0x90 and b2 <= 0xBF) or (b1 >= 0xF1 and b1 <= 0xF3 and b2 >= 0x80 and b2 <= 0xBF) or (b1 == 0xF4 and b2 >= 0x80 and b2 <= 0x8F) then
                    err_len = (b3 >= 0x80 and b3 <= 0xBF) and 3 or 2
                end
                local exc = build_decode_exc("utf-8", data, i, i + err_len, "invalid continuation byte")
                local repl, next_i = invoke_error_handler(errors, exc)
                out[#out + 1] = repl; i = next_i
            end
        else
            local reason = (b1 >= 0xC2 and b1 <= 0xF4) and "unexpected end of data" or "invalid start byte"
            local exc = build_decode_exc("utf-8", data, i, i + 1, reason)
            local repl, next_i = invoke_error_handler(errors, exc)
            out[#out + 1] = repl; i = next_i
        end
    end
    return table.concat(out)
end

local function encode_utf8(str, errors, add_bom)
    local out = add_bom and { "\239\187\191" } or {}
    local len, i = mw.ustring.len(str), 1
    while i <= len do
        local cp = mw.ustring.codepoint(str, i)
        if cp >= 0xD800 and cp <= 0xDFFF then
            local exc = build_encode_exc("utf-8", str, i, i + 1, "surrogates not allowed")
            local repl, next_i = invoke_error_handler(errors, exc)
            out[#out + 1] = repl; i = next_i
        else
            out[#out + 1] = mw.ustring.char(cp); i = i + 1
        end
    end
    return table.concat(out)
end

local function decode_ascii(data, errors)
    local out, len, i = {}, #data, 1
    while i <= len do
        local b = string.byte(data, i)
        if b <= 0x7F then
            out[#out + 1] = string.char(b); i = i + 1
        else
            local exc = build_decode_exc("ascii", data, i, i + 1, "ordinal not in range(128)")
            local repl, next_i = invoke_error_handler(errors, exc)
            out[#out + 1] = repl; i = next_i
        end
    end
    return table.concat(out)
end

local function encode_ascii(str, errors)
    local out, len, i = {}, mw.ustring.len(str), 1
    while i <= len do
        local cp = mw.ustring.codepoint(str, i)
        if cp <= 0x7F then
            out[#out + 1] = string.char(cp); i = i + 1
        else
            local exc = build_encode_exc("ascii", str, i, i + 1, "ordinal not in range(128)")
            local repl, next_i = invoke_error_handler(errors, exc)
            out[#out + 1] = repl; i = next_i
        end
    end
    return table.concat(out)
end

local function decode_latin1(data)
    local out = {}
    for i = 1, #data do
        local b = string.byte(data, i)
        if b <= 0x7F then out[i] = string.char(b)
        else out[i] = string.char(0xC0 + math.floor(b / 64), 0x80 + (b % 64)) end
    end
    return table.concat(out)
end

local function encode_latin1(str, errors)
    local out, len, i = {}, mw.ustring.len(str), 1
    while i <= len do
        local cp = mw.ustring.codepoint(str, i)
        if cp <= 0xFF then
            out[#out + 1] = string.char(cp); i = i + 1
        else
            local exc = build_encode_exc("latin-1", str, i, i + 1, "ordinal not in range(256)")
            local repl, next_i = invoke_error_handler(errors, exc)
            out[#out + 1] = repl; i = next_i
        end
    end
    return table.concat(out)
end

-- ==========================================
-- 4. 對外核心 API 與模組建構
-- ==========================================
function Codecs.lua_decode(obj, encoding, errors)
    encoding = encoding or "utf-8"
    errors = errors or "strict"
    if type(obj) == "table" and obj.__type == "instance" and obj.__builtin_value ~= nil then obj = obj.__builtin_value end
    if type(obj) == "table" and (obj.__type == "bytes" or obj.__type == "bytearray" or obj.__type == "memoryview") then
        if obj.__type == "memoryview" and obj._released then
            Runtime.raise("ValueError", "operation forbidden on released memoryview object")
        end
        obj = obj._data
    end
    if type(obj) ~= "string" then Runtime.raise("TypeError", "a bytes-like object is required") end

    local raw_enc = tostring(encoding):lower():gsub("_", "-")
    local is_sig = (raw_enc == "utf-8-sig")
    local enc = normalize_encoding(encoding)
    if enc == "utf-8" then return decode_utf8(obj, errors, is_sig)
    elseif enc == "ascii" then return decode_ascii(obj, errors)
    elseif enc == "latin-1" then return decode_latin1(obj)
    elseif enc == "base64" or enc == "base64-codec" then
        return require("Module:沙盒/a2569875/PythonScript/Librarys/StdAlgorithms").base64().raw_b64decode(obj, nil, false)
    elseif enc == "hex" or enc == "hex-codec" then
        return require("Module:沙盒/a2569875/PythonScript/Librarys/StdAlgorithms").base64().raw_b16decode(obj, true)
    end
    Runtime.raise("LookupError", "unknown encoding: " .. tostring(encoding))
end

function Codecs.lua_encode(obj, encoding, errors)
    encoding = encoding or "utf-8"
    errors = errors or "strict"
    if type(obj) == "table" and obj.__type == "instance" and obj.__builtin_value ~= nil then obj = obj.__builtin_value end
    if type(obj) ~= "string" then Runtime.raise("TypeError", "encoding with '" .. tostring(encoding) .. "' codec failed") end

    local raw_enc = tostring(encoding):lower():gsub("_", "-")
    local is_sig = (raw_enc == "utf-8-sig")
    local enc, res = normalize_encoding(encoding), nil
    if enc == "utf-8" then res = encode_utf8(obj, errors, is_sig)
    elseif enc == "ascii" then res = encode_ascii(obj, errors)
    elseif enc == "latin-1" then res = encode_latin1(obj, errors)
    elseif enc == "base64" or enc == "base64-codec" then
        res = require("Module:沙盒/a2569875/PythonScript/Librarys/StdAlgorithms").base64().raw_b64encode(obj)
    elseif enc == "hex" or enc == "hex-codec" then
        res = require("Module:沙盒/a2569875/PythonScript/Librarys/StdAlgorithms").base64().raw_b16encode(obj):lower()
    else
        Runtime.raise("LookupError", "unknown encoding: " .. tostring(encoding))
    end
    return { __type = "bytes", _data = res }
end

function Codecs.get_module()
    local mod = PyClass.make_instance(PyClass.make_class("module", {}, {}))
    local dict = {
        encode = { __type = "builtin_method", __name = "encode", __call = function(a, kw)
            local offset = (a[1] == mod) and 1 or 0
            local obj = a[offset + 1]
            local encoding = (a[offset + 2] ~= nil) and a[offset + 2] or (kw and kw.encoding) or "utf-8"
            local errors = (a[offset + 3] ~= nil) and a[offset + 3] or (kw and kw.errors) or "strict"
            return Codecs.lua_encode(obj, encoding, errors)
        end },
        decode = { __type = "builtin_method", __name = "decode", __call = function(a, kw)
            local offset = (a[1] == mod) and 1 or 0
            local obj = a[offset + 1]
            local encoding = (a[offset + 2] ~= nil) and a[offset + 2] or (kw and kw.encoding) or "utf-8"
            local errors = (a[offset + 3] ~= nil) and a[offset + 3] or (kw and kw.errors) or "strict"
            return Codecs.lua_decode(obj, encoding, errors)
        end },
        lookup_error = { __type = "builtin_method", __name = "lookup_error", __call = function(a)
            local offset = (a[1] == mod) and 1 or 0
            return Codecs.lookup_error(a[offset + 1])
        end },
        register_error = { __type = "builtin_method", __name = "register_error", __call = function(a)
            local offset = (a[1] == mod) and 1 or 0
            Codecs.register_error(a[offset + 1], a[offset + 2])
            return Runtime.builtins.None
        end },
        BOM_UTF8 = { __type = "bytes", _data = "\239\187\191" },
        BOM_LE = { __type = "bytes", _data = "\255\254" },
        BOM_BE = { __type = "bytes", _data = "\254\255" }
    }
    -- 掛載於實例 __dict 上,與 base64 等模組一致
    for k, v in pairs(dict) do mod.__dict[k] = v end
    mod.__dict["__name__"] = "codecs"
    mod.__dict["__all__"] = { __type = "list", elements = { "encode", "decode", "lookup_error", "register_error", "BOM_UTF8", "BOM_LE", "BOM_BE" } }
    return mod
end

return { --保留未來其它模組併入的可能性
	Codecs=Codecs
}