- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library. - Implemented core functionalities including image listing, retrieval, and preview serving. - Added support for OAuth 2.0 authentication with token generation. - Created a simple HTTP router to handle API requests. - Included JSON encoding/decoding utilities for API responses. - Developed a script to launch the API server alongside darktable, ensuring seamless integration. - Added necessary configuration and setup for the API server, including CORS support and error handling.
156 lines
5.0 KiB
Lua
156 lines
5.0 KiB
Lua
-- Minimal JSON encoder/decoder (pure Lua, no dependencies)
|
|
local M = {}
|
|
|
|
-- ── Encoder ──────────────────────────────────────────────────────────────────
|
|
|
|
-- Iterate byte-by-byte to avoid null-byte issues in Lua pattern ranges.
|
|
local function encode_str(s)
|
|
local buf = {'"'}
|
|
for i = 1, #s do
|
|
local b = s:byte(i)
|
|
if b == 34 then buf[#buf+1] = '\\"'
|
|
elseif b == 92 then buf[#buf+1] = '\\\\'
|
|
elseif b == 10 then buf[#buf+1] = '\\n'
|
|
elseif b == 13 then buf[#buf+1] = '\\r'
|
|
elseif b == 9 then buf[#buf+1] = '\\t'
|
|
elseif b < 32 or b == 127 then
|
|
buf[#buf+1] = string.format('\\u%04x', b)
|
|
else
|
|
buf[#buf+1] = s:sub(i, i)
|
|
end
|
|
end
|
|
buf[#buf+1] = '"'
|
|
return table.concat(buf)
|
|
end
|
|
|
|
local function enc(v, depth)
|
|
if depth > 64 then error("json.encode: table too deeply nested") end
|
|
local t = type(v)
|
|
if t == "nil" then return "null"
|
|
elseif t == "boolean" then return tostring(v)
|
|
elseif t == "number" then
|
|
if v ~= v or math.abs(v) == math.huge then return "null" end
|
|
return v == math.floor(v) and string.format("%d", v) or string.format("%.10g", v)
|
|
elseif t == "string" then
|
|
return encode_str(v)
|
|
elseif t == "table" then
|
|
-- Treat as array when it has a consecutive integer key sequence from 1
|
|
if #v > 0 then
|
|
local parts = {}
|
|
for i = 1, #v do parts[i] = enc(v[i], depth + 1) end
|
|
return "[" .. table.concat(parts, ",") .. "]"
|
|
end
|
|
local parts = {}
|
|
for k, x in pairs(v) do
|
|
if type(k) == "string" then
|
|
parts[#parts+1] = encode_str(k) .. ":" .. enc(x, depth + 1)
|
|
end
|
|
end
|
|
return "{" .. table.concat(parts, ",") .. "}"
|
|
end
|
|
return "null"
|
|
end
|
|
|
|
function M.encode(v) return enc(v, 0) end
|
|
|
|
-- ── Decoder ───────────────────────────────────────────────────────────────────
|
|
|
|
function M.decode(src)
|
|
local pos = 1
|
|
|
|
local function skip()
|
|
while pos <= #src and src:byte(pos) <= 32 do pos = pos + 1 end
|
|
end
|
|
|
|
local parse -- forward declaration
|
|
|
|
local function parse_string()
|
|
pos = pos + 1 -- consume opening "
|
|
local buf = {}
|
|
while pos <= #src do
|
|
local c = src:sub(pos, pos)
|
|
if c == '"' then pos = pos + 1; return table.concat(buf) end
|
|
if c == '\\' then
|
|
pos = pos + 1
|
|
c = src:sub(pos, pos)
|
|
if c == '"' then buf[#buf+1] = '"'
|
|
elseif c == '\\' then buf[#buf+1] = '\\'
|
|
elseif c == '/' then buf[#buf+1] = '/'
|
|
elseif c == 'n' then buf[#buf+1] = '\n'
|
|
elseif c == 'r' then buf[#buf+1] = '\r'
|
|
elseif c == 't' then buf[#buf+1] = '\t'
|
|
elseif c == 'b' then buf[#buf+1] = '\b'
|
|
elseif c == 'f' then buf[#buf+1] = '\f'
|
|
elseif c == 'u' then
|
|
local hex = tonumber(src:sub(pos+1, pos+4), 16) or 0
|
|
-- Only emit ASCII; multi-byte unicode emits a placeholder
|
|
buf[#buf+1] = hex < 128 and string.char(hex) or '\xEF\xBF\xBD'
|
|
pos = pos + 4
|
|
end
|
|
else
|
|
buf[#buf+1] = c
|
|
end
|
|
pos = pos + 1
|
|
end
|
|
error("json.decode: unterminated string near pos " .. pos)
|
|
end
|
|
|
|
local function parse_object()
|
|
pos = pos + 1 -- consume '{'
|
|
local obj = {}
|
|
skip()
|
|
if src:sub(pos, pos) == '}' then pos = pos + 1; return obj end
|
|
while true do
|
|
skip()
|
|
local key = parse_string()
|
|
skip()
|
|
if src:sub(pos, pos) ~= ':' then error("json.decode: expected ':' near pos " .. pos) end
|
|
pos = pos + 1
|
|
skip()
|
|
obj[key] = parse()
|
|
skip()
|
|
local ch = src:sub(pos, pos); pos = pos + 1
|
|
if ch == '}' then return obj end
|
|
if ch ~= ',' then error("json.decode: expected ',' or '}' near pos " .. pos) end
|
|
end
|
|
end
|
|
|
|
local function parse_array()
|
|
pos = pos + 1 -- consume '['
|
|
local arr = {}
|
|
skip()
|
|
if src:sub(pos, pos) == ']' then pos = pos + 1; return arr end
|
|
while true do
|
|
skip()
|
|
arr[#arr+1] = parse()
|
|
skip()
|
|
local ch = src:sub(pos, pos); pos = pos + 1
|
|
if ch == ']' then return arr end
|
|
if ch ~= ',' then error("json.decode: expected ',' or ']' near pos " .. pos) end
|
|
end
|
|
end
|
|
|
|
parse = function()
|
|
skip()
|
|
local c = src:sub(pos, pos)
|
|
if c == '"' then return parse_string()
|
|
elseif c == '{' then return parse_object()
|
|
elseif c == '[' then return parse_array()
|
|
elseif c == 't' then pos = pos + 4; return true
|
|
elseif c == 'f' then pos = pos + 5; return false
|
|
elseif c == 'n' then pos = pos + 4; return nil
|
|
else
|
|
-- Parentheses create the capture; without them find returns only start/end.
|
|
local _, e, n = src:find("^(-?%d+%.?%d*[eE]?[+-]?%d*)", pos)
|
|
if n then pos = e + 1; return tonumber(n) end
|
|
error("json.decode: unexpected token '" .. c .. "' at pos " .. pos)
|
|
end
|
|
end
|
|
|
|
local ok, result = pcall(parse)
|
|
if not ok then return nil, result end
|
|
return result
|
|
end
|
|
|
|
return M
|